From 7dfb9503e74b137cb85a8afb173be79e2215403c Mon Sep 17 00:00:00 2001 From: Katerina Chinnappan Date: Wed, 10 Jan 2024 11:32:16 -0800 Subject: [PATCH 1/8] adding prospect-api from a679afe4108f3651c33427f16b2fb02c46afe3a1 --- .circleci/branch-filters.yml | 45 + .circleci/config.yml | 4 +- .circleci/monorepo-jobs.yml | 34 + .circleci/prospect-api.yml | 247 + .circleci/scripts/setup.sh | 13 + .circleci/scripts/setup_aws.sh | 9 + .circleci/shared.yml | 710 + .docker/aws/dynamodb.sh | 31 + .../dynamodb/prospect_api_prospects.json | 0 .docker/{localstack => aws}/s3.sh | 0 .docker/local.env | 10 + .docker/localstack/dynamodb.sh | 15 - .idea/vcs.xml | 1 + .npmrc | 1 - .nvmrc | 2 +- .prettierignore | 1 + .prettierrc | 6 + Dockerfile | 4 +- README.md | 15 +- docker-compose.yml | 4 +- example.env | 10 + infrastructure/prospect-api/.eslintrc.js | 3 + .../prospect-api/.terraform-version | 1 + infrastructure/prospect-api/cdktf.json | 5 + infrastructure/prospect-api/package.json | 24 + .../prospect-api/src/config/index.ts | 54 + infrastructure/prospect-api/src/dynamodb.ts | 62 + infrastructure/prospect-api/src/main.ts | 310 + infrastructure/prospect-api/src/sqsLambda.ts | 101 + infrastructure/prospect-api/tsconfig.json | 9 + lambdas/prospect-api-lambda/.eslintrc.js | 3 + lambdas/prospect-api-lambda/jest.config.js | 7 + lambdas/prospect-api-lambda/jest.setup.js | 3 + lambdas/prospect-api-lambda/package.json | 39 + lambdas/prospect-api-lambda/src/config.ts | 21 + .../src/dynamodb/lib.integration.ts | 314 + .../prospect-api-lambda/src/dynamodb/lib.ts | 161 + lambdas/prospect-api-lambda/src/index.ts | 162 + lambdas/prospect-api-lambda/src/lib.spec.ts | 379 + lambdas/prospect-api-lambda/src/lib.ts | 250 + lambdas/prospect-api-lambda/src/types.ts | 10 + lambdas/prospect-api-lambda/tsconfig.json | 17 + package.json | 13 +- packages/eslint-config-custom/cdktf.js | 50 + packages/eslint-config-custom/lambda.js | 31 + packages/eslint-config-custom/library.js | 53 + .../eslint-config-custom/react-internal.js | 2 +- packages/prospectapi-common/.eslintrc.js | 3 + .../prospectapi-common/client-api-proxy.ts | 89 + packages/prospectapi-common/config.ts | 22 + .../prospectapi-common/dynamodb-client.ts | 33 + .../dynamodb.integration.ts | 33 + packages/prospectapi-common/dynamodb.spec.ts | 47 + packages/prospectapi-common/dynamodb.ts | 133 + packages/prospectapi-common/index.d.ts | 21 + packages/prospectapi-common/index.js | 22 + packages/prospectapi-common/index.js.map | 1 + packages/prospectapi-common/index.ts | 25 + packages/prospectapi-common/jest.config.js | 7 + packages/prospectapi-common/jest.setup.js | 3 + packages/prospectapi-common/lib.spec.ts | 286 + packages/prospectapi-common/lib.ts | 161 + packages/prospectapi-common/package.json | 35 + packages/prospectapi-common/test/helpers.ts | 59 + packages/prospectapi-common/tsconfig.json | 8 + packages/prospectapi-common/types.ts | 282 + packages/terraform-modules/.eslintrc.js | 3 + packages/terraform-modules/README.md | 74 + packages/terraform-modules/cdktf.json | 5 + packages/terraform-modules/jest.config.js | 10 + packages/terraform-modules/package.json | 40 + packages/terraform-modules/setup.js | 2 + .../src/base/ApplicationAutoscaling.spec.ts | 139 + .../src/base/ApplicationAutoscaling.ts | 202 + .../src/base/ApplicationBackups.spec.ts | 74 + .../src/base/ApplicationBackups.ts | 76 + .../src/base/ApplicationBaseDNS.spec.ts | 81 + .../src/base/ApplicationBaseDNS.ts | 85 + .../src/base/ApplicationCertificate.spec.ts | 126 + .../src/base/ApplicationCertificate.ts | 118 + .../src/base/ApplicationDynamoDBTable.spec.ts | 239 + .../src/base/ApplicationDynamoDBTable.ts | 391 + .../src/base/ApplicationECR.spec.ts | 26 + .../src/base/ApplicationECR.ts | 63 + .../base/ApplicationECSAlbCodeDeploy.spec.ts | 141 + .../src/base/ApplicationECSAlbCodeDeploy.ts | 206 + .../src/base/ApplicationECSCluster.spec.ts | 26 + .../src/base/ApplicationECSCluster.ts | 35 + .../ApplicationECSContainerDefinition.spec.ts | 178 + .../base/ApplicationECSContainerDefinition.ts | 137 + .../src/base/ApplicationECSIAM.spec.ts | 30 + .../src/base/ApplicationECSIAM.ts | 138 + .../src/base/ApplicationECSService.spec.ts | 323 + .../src/base/ApplicationECSService.ts | 611 + .../src/base/ApplicationElasticacheCluster.ts | 174 + .../base/ApplicationEventBridgeRule.spec.ts | 104 + .../src/base/ApplicationEventBridgeRule.ts | 103 + .../src/base/ApplicationEventBus.spec.ts | 18 + .../src/base/ApplicationEventBus.ts | 30 + .../base/ApplicationLambdaCodeDeploy.spec.ts | 82 + .../src/base/ApplicationLambdaCodeDeploy.ts | 158 + ...licationLambdaSnsTopicSubscription.spec.ts | 38 + .../ApplicationLambdaSnsTopicSubscription.ts | 155 + .../src/base/ApplicationLoadBalancer.spec.ts | 132 + .../src/base/ApplicationLoadBalancer.ts | 201 + .../src/base/ApplicationMemcache.spec.ts | 48 + .../src/base/ApplicationMemcache.ts | 82 + .../src/base/ApplicationRDSCluster.spec.ts | 65 + .../src/base/ApplicationRDSCluster.ts | 217 + .../src/base/ApplicationRedis.spec.ts | 48 + .../src/base/ApplicationRedis.ts | 96 + .../src/base/ApplicationSQSQueue.spec.ts | 72 + .../src/base/ApplicationSQSQueue.ts | 146 + ...ApplicationSqsSnsTopicSubscription.spec.ts | 55 + .../ApplicationSqsSnsTopicSubscription.ts | 122 + .../src/base/ApplicationTargetGroup.spec.ts | 30 + .../src/base/ApplicationTargetGroup.ts | 40 + .../base/ApplicationVersionedLambda.spec.ts | 137 + .../src/base/ApplicationVersionedLambda.ts | 280 + .../ApplicationAutoscaling.spec.ts.snap | 321 + .../ApplicationBackups.spec.ts.snap | 105 + .../ApplicationBaseDNS.spec.ts.snap | 120 + .../ApplicationCertificate.spec.ts.snap | 239 + .../ApplicationDynamoDBTable.spec.ts.snap | 1388 ++ .../__snapshots__/ApplicationECR.spec.ts.snap | 53 + .../ApplicationECSAlbCodeDeploy.spec.ts.snap | 908 + .../ApplicationECSCluster.spec.ts.snap | 41 + .../ApplicationECSIAM.spec.ts.snap | 101 + .../ApplicationECSService.spec.ts.snap | 1554 ++ .../ApplicationEventBridgeRule.spec.ts.snap | 152 + .../ApplicationEventBus.spec.ts.snap | 16 + .../ApplicationLambdaCodeDeploy.spec.ts.snap | 505 + ...ionLambdaSnsTopicSubscription.spec.ts.snap | 162 + .../ApplicationLoadBalancer.spec.ts.snap | 627 + .../ApplicationMemcache.spec.ts.snap | 223 + .../ApplicationRDSCluster.spec.ts.snap | 359 + .../ApplicationRedis.spec.ts.snap | 229 + .../ApplicationSQSQueue.spec.ts.snap | 45 + ...cationSqsSnsTopicSubscription.spec.ts.snap | 322 + .../ApplicationTargetGroup.spec.ts.snap | 53 + .../ApplicationVersionedLambda.spec.ts.snap | 1776 ++ packages/terraform-modules/src/example.ts | 170 + packages/terraform-modules/src/index.ts | 37 + .../src/pocket/PocketALBApplication.spec.ts | 429 + .../src/pocket/PocketALBApplication.ts | 1016 ++ .../PocketApiGatewayLambdaIntegration.spec.ts | 48 + .../PocketApiGatewayLambdaIntegration.ts | 245 + .../pocket/PocketCloudwatchSynthetics.spec.ts | 47 + .../src/pocket/PocketCloudwatchSynthetics.ts | 333 + .../src/pocket/PocketECSApplication.spec.ts | 135 + .../src/pocket/PocketECSApplication.ts | 382 + .../src/pocket/PocketECSCodePipeline.spec.ts | 145 + .../src/pocket/PocketECSCodePipeline.ts | 368 + ...EventBridgeRuleWithMultipleTargets.spec.ts | 144 + ...ocketEventBridgeRuleWithMultipleTargets.ts | 76 + .../PocketEventBridgeWithLambdaTarget.spec.ts | 40 + .../PocketEventBridgeWithLambdaTarget.ts | 86 + .../src/pocket/PocketPagerDuty.spec.ts | 73 + .../src/pocket/PocketPagerDuty.ts | 177 + .../pocket/PocketSQSWithLambdaTarget.spec.ts | 80 + .../src/pocket/PocketSQSWithLambdaTarget.ts | 195 + .../src/pocket/PocketSynthetics.spec.ts | 22 + .../src/pocket/PocketSynthetics.ts | 112 + .../src/pocket/PocketVPC.spec.ts | 13 + .../terraform-modules/src/pocket/PocketVPC.ts | 132 + .../src/pocket/PocketVersionedLambda.spec.ts | 283 + .../src/pocket/PocketVersionedLambda.ts | 204 + .../PocketALBApplication.spec.ts.snap | 10737 ++++++++++++ ...etApiGatewayLambdaIntegration.spec.ts.snap | 303 + .../PocketCloudwatchSynthetics.spec.ts.snap | 469 + .../PocketECSApplication.spec.ts.snap | 2106 +++ .../PocketECSCodePipeline.spec.ts.snap | 2146 +++ ...BridgeRuleWithMultipleTargets.spec.ts.snap | 269 + ...etEventBridgeWithLambdaTarget.spec.ts.snap | 386 + .../PocketPagerDuty.spec.ts.snap | 568 + .../PocketSQSWithLambdaTarget.spec.ts.snap | 605 + .../PocketSynthetics.spec.ts.snap | 93 + .../__snapshots__/PocketVPC.spec.ts.snap | 108 + .../PocketVersionedLambda.spec.ts.snap | 2687 +++ packages/terraform-modules/src/testHelpers.ts | 10 + .../terraform-modules/src/utilities.jest.ts | 31 + packages/terraform-modules/src/utilities.ts | 25 + packages/terraform-modules/tsconfig.json | 9 + packages/tsconfig/cdktf.json | 22 + packages/tsconfig/lambda.json | 12 + packages/tsconfig/library.json | 22 + packages/tsconfig/package.json | 21 +- pnpm-lock.yaml | 13883 ++++++++++++++-- pnpm-workspace.yaml | 4 +- servers/prospect-api/.eslintrc.js | 3 + servers/prospect-api/README.md | 159 + servers/prospect-api/jest.config.js | 10 + servers/prospect-api/jest.setup.js | 4 + servers/prospect-api/package.json | 52 + servers/prospect-api/schema.graphql | 120 + .../src/aws/dynamodb/lib.integration.ts | 748 + servers/prospect-api/src/aws/dynamodb/lib.ts | 164 + .../prospect-api/src/aws/eventBridgeClient.ts | 6 + servers/prospect-api/src/config/index.ts | 44 + servers/prospect-api/src/context.ts | 45 + .../prospect-api/src/events/events.spec.ts | 196 + servers/prospect-api/src/events/events.ts | 119 + .../src/events/snowplow-test-helpers.ts | 62 + .../src/events/snowplow.integration.ts | 48 + servers/prospect-api/src/events/snowplow.ts | 136 + servers/prospect-api/src/events/types.ts | 76 + servers/prospect-api/src/express.ts | 54 + servers/prospect-api/src/lib.spec.ts | 380 + servers/prospect-api/src/lib.ts | 280 + servers/prospect-api/src/main.ts | 6 + servers/prospect-api/src/resolvers.ts | 165 + servers/prospect-api/src/seeder.ts | 80 + servers/prospect-api/src/server.ts | 75 + .../src/test/admin-server/fragments.gql.ts | 26 + .../src/test/admin-server/index.ts | 27 + .../src/test/admin-server/mutations.gql.ts | 26 + .../src/test/admin-server/queries.gql.ts | 14 + servers/prospect-api/src/typeDefs.ts | 7 + servers/prospect-api/src/types.ts | 69 + servers/prospect-api/tsconfig.json | 15 + turbo.json | 7 + 221 files changed, 60958 insertions(+), 1800 deletions(-) create mode 100644 .circleci/branch-filters.yml create mode 100644 .circleci/monorepo-jobs.yml create mode 100644 .circleci/prospect-api.yml create mode 100755 .circleci/scripts/setup.sh create mode 100755 .circleci/scripts/setup_aws.sh create mode 100644 .circleci/shared.yml create mode 100755 .docker/aws/dynamodb.sh rename .docker/{localstack => aws}/dynamodb/prospect_api_prospects.json (100%) rename .docker/{localstack => aws}/s3.sh (100%) mode change 100644 => 100755 create mode 100644 .docker/local.env delete mode 100644 .docker/localstack/dynamodb.sh create mode 100644 .prettierignore create mode 100644 .prettierrc create mode 100644 example.env create mode 100644 infrastructure/prospect-api/.eslintrc.js create mode 100644 infrastructure/prospect-api/.terraform-version create mode 100644 infrastructure/prospect-api/cdktf.json create mode 100644 infrastructure/prospect-api/package.json create mode 100644 infrastructure/prospect-api/src/config/index.ts create mode 100644 infrastructure/prospect-api/src/dynamodb.ts create mode 100644 infrastructure/prospect-api/src/main.ts create mode 100644 infrastructure/prospect-api/src/sqsLambda.ts create mode 100644 infrastructure/prospect-api/tsconfig.json create mode 100644 lambdas/prospect-api-lambda/.eslintrc.js create mode 100644 lambdas/prospect-api-lambda/jest.config.js create mode 100644 lambdas/prospect-api-lambda/jest.setup.js create mode 100644 lambdas/prospect-api-lambda/package.json create mode 100644 lambdas/prospect-api-lambda/src/config.ts create mode 100644 lambdas/prospect-api-lambda/src/dynamodb/lib.integration.ts create mode 100644 lambdas/prospect-api-lambda/src/dynamodb/lib.ts create mode 100644 lambdas/prospect-api-lambda/src/index.ts create mode 100644 lambdas/prospect-api-lambda/src/lib.spec.ts create mode 100644 lambdas/prospect-api-lambda/src/lib.ts create mode 100644 lambdas/prospect-api-lambda/src/types.ts create mode 100644 lambdas/prospect-api-lambda/tsconfig.json create mode 100644 packages/eslint-config-custom/cdktf.js create mode 100644 packages/eslint-config-custom/lambda.js create mode 100644 packages/eslint-config-custom/library.js create mode 100644 packages/prospectapi-common/.eslintrc.js create mode 100644 packages/prospectapi-common/client-api-proxy.ts create mode 100644 packages/prospectapi-common/config.ts create mode 100644 packages/prospectapi-common/dynamodb-client.ts create mode 100644 packages/prospectapi-common/dynamodb.integration.ts create mode 100644 packages/prospectapi-common/dynamodb.spec.ts create mode 100644 packages/prospectapi-common/dynamodb.ts create mode 100644 packages/prospectapi-common/index.d.ts create mode 100644 packages/prospectapi-common/index.js create mode 100644 packages/prospectapi-common/index.js.map create mode 100644 packages/prospectapi-common/index.ts create mode 100644 packages/prospectapi-common/jest.config.js create mode 100644 packages/prospectapi-common/jest.setup.js create mode 100644 packages/prospectapi-common/lib.spec.ts create mode 100644 packages/prospectapi-common/lib.ts create mode 100644 packages/prospectapi-common/package.json create mode 100644 packages/prospectapi-common/test/helpers.ts create mode 100644 packages/prospectapi-common/tsconfig.json create mode 100644 packages/prospectapi-common/types.ts create mode 100644 packages/terraform-modules/.eslintrc.js create mode 100644 packages/terraform-modules/README.md create mode 100644 packages/terraform-modules/cdktf.json create mode 100644 packages/terraform-modules/jest.config.js create mode 100644 packages/terraform-modules/package.json create mode 100644 packages/terraform-modules/setup.js create mode 100644 packages/terraform-modules/src/base/ApplicationAutoscaling.spec.ts create mode 100644 packages/terraform-modules/src/base/ApplicationAutoscaling.ts create mode 100644 packages/terraform-modules/src/base/ApplicationBackups.spec.ts create mode 100644 packages/terraform-modules/src/base/ApplicationBackups.ts create mode 100644 packages/terraform-modules/src/base/ApplicationBaseDNS.spec.ts create mode 100644 packages/terraform-modules/src/base/ApplicationBaseDNS.ts create mode 100644 packages/terraform-modules/src/base/ApplicationCertificate.spec.ts create mode 100644 packages/terraform-modules/src/base/ApplicationCertificate.ts create mode 100644 packages/terraform-modules/src/base/ApplicationDynamoDBTable.spec.ts create mode 100644 packages/terraform-modules/src/base/ApplicationDynamoDBTable.ts create mode 100644 packages/terraform-modules/src/base/ApplicationECR.spec.ts create mode 100644 packages/terraform-modules/src/base/ApplicationECR.ts create mode 100644 packages/terraform-modules/src/base/ApplicationECSAlbCodeDeploy.spec.ts create mode 100644 packages/terraform-modules/src/base/ApplicationECSAlbCodeDeploy.ts create mode 100644 packages/terraform-modules/src/base/ApplicationECSCluster.spec.ts create mode 100644 packages/terraform-modules/src/base/ApplicationECSCluster.ts create mode 100644 packages/terraform-modules/src/base/ApplicationECSContainerDefinition.spec.ts create mode 100644 packages/terraform-modules/src/base/ApplicationECSContainerDefinition.ts create mode 100644 packages/terraform-modules/src/base/ApplicationECSIAM.spec.ts create mode 100644 packages/terraform-modules/src/base/ApplicationECSIAM.ts create mode 100644 packages/terraform-modules/src/base/ApplicationECSService.spec.ts create mode 100644 packages/terraform-modules/src/base/ApplicationECSService.ts create mode 100644 packages/terraform-modules/src/base/ApplicationElasticacheCluster.ts create mode 100644 packages/terraform-modules/src/base/ApplicationEventBridgeRule.spec.ts create mode 100644 packages/terraform-modules/src/base/ApplicationEventBridgeRule.ts create mode 100644 packages/terraform-modules/src/base/ApplicationEventBus.spec.ts create mode 100644 packages/terraform-modules/src/base/ApplicationEventBus.ts create mode 100644 packages/terraform-modules/src/base/ApplicationLambdaCodeDeploy.spec.ts create mode 100644 packages/terraform-modules/src/base/ApplicationLambdaCodeDeploy.ts create mode 100644 packages/terraform-modules/src/base/ApplicationLambdaSnsTopicSubscription.spec.ts create mode 100644 packages/terraform-modules/src/base/ApplicationLambdaSnsTopicSubscription.ts create mode 100644 packages/terraform-modules/src/base/ApplicationLoadBalancer.spec.ts create mode 100644 packages/terraform-modules/src/base/ApplicationLoadBalancer.ts create mode 100644 packages/terraform-modules/src/base/ApplicationMemcache.spec.ts create mode 100644 packages/terraform-modules/src/base/ApplicationMemcache.ts create mode 100644 packages/terraform-modules/src/base/ApplicationRDSCluster.spec.ts create mode 100644 packages/terraform-modules/src/base/ApplicationRDSCluster.ts create mode 100644 packages/terraform-modules/src/base/ApplicationRedis.spec.ts create mode 100644 packages/terraform-modules/src/base/ApplicationRedis.ts create mode 100644 packages/terraform-modules/src/base/ApplicationSQSQueue.spec.ts create mode 100644 packages/terraform-modules/src/base/ApplicationSQSQueue.ts create mode 100644 packages/terraform-modules/src/base/ApplicationSqsSnsTopicSubscription.spec.ts create mode 100644 packages/terraform-modules/src/base/ApplicationSqsSnsTopicSubscription.ts create mode 100644 packages/terraform-modules/src/base/ApplicationTargetGroup.spec.ts create mode 100644 packages/terraform-modules/src/base/ApplicationTargetGroup.ts create mode 100644 packages/terraform-modules/src/base/ApplicationVersionedLambda.spec.ts create mode 100644 packages/terraform-modules/src/base/ApplicationVersionedLambda.ts create mode 100644 packages/terraform-modules/src/base/__snapshots__/ApplicationAutoscaling.spec.ts.snap create mode 100644 packages/terraform-modules/src/base/__snapshots__/ApplicationBackups.spec.ts.snap create mode 100644 packages/terraform-modules/src/base/__snapshots__/ApplicationBaseDNS.spec.ts.snap create mode 100644 packages/terraform-modules/src/base/__snapshots__/ApplicationCertificate.spec.ts.snap create mode 100644 packages/terraform-modules/src/base/__snapshots__/ApplicationDynamoDBTable.spec.ts.snap create mode 100644 packages/terraform-modules/src/base/__snapshots__/ApplicationECR.spec.ts.snap create mode 100644 packages/terraform-modules/src/base/__snapshots__/ApplicationECSAlbCodeDeploy.spec.ts.snap create mode 100644 packages/terraform-modules/src/base/__snapshots__/ApplicationECSCluster.spec.ts.snap create mode 100644 packages/terraform-modules/src/base/__snapshots__/ApplicationECSIAM.spec.ts.snap create mode 100644 packages/terraform-modules/src/base/__snapshots__/ApplicationECSService.spec.ts.snap create mode 100644 packages/terraform-modules/src/base/__snapshots__/ApplicationEventBridgeRule.spec.ts.snap create mode 100644 packages/terraform-modules/src/base/__snapshots__/ApplicationEventBus.spec.ts.snap create mode 100644 packages/terraform-modules/src/base/__snapshots__/ApplicationLambdaCodeDeploy.spec.ts.snap create mode 100644 packages/terraform-modules/src/base/__snapshots__/ApplicationLambdaSnsTopicSubscription.spec.ts.snap create mode 100644 packages/terraform-modules/src/base/__snapshots__/ApplicationLoadBalancer.spec.ts.snap create mode 100644 packages/terraform-modules/src/base/__snapshots__/ApplicationMemcache.spec.ts.snap create mode 100644 packages/terraform-modules/src/base/__snapshots__/ApplicationRDSCluster.spec.ts.snap create mode 100644 packages/terraform-modules/src/base/__snapshots__/ApplicationRedis.spec.ts.snap create mode 100644 packages/terraform-modules/src/base/__snapshots__/ApplicationSQSQueue.spec.ts.snap create mode 100644 packages/terraform-modules/src/base/__snapshots__/ApplicationSqsSnsTopicSubscription.spec.ts.snap create mode 100644 packages/terraform-modules/src/base/__snapshots__/ApplicationTargetGroup.spec.ts.snap create mode 100644 packages/terraform-modules/src/base/__snapshots__/ApplicationVersionedLambda.spec.ts.snap create mode 100644 packages/terraform-modules/src/example.ts create mode 100644 packages/terraform-modules/src/index.ts create mode 100644 packages/terraform-modules/src/pocket/PocketALBApplication.spec.ts create mode 100644 packages/terraform-modules/src/pocket/PocketALBApplication.ts create mode 100644 packages/terraform-modules/src/pocket/PocketApiGatewayLambdaIntegration.spec.ts create mode 100644 packages/terraform-modules/src/pocket/PocketApiGatewayLambdaIntegration.ts create mode 100644 packages/terraform-modules/src/pocket/PocketCloudwatchSynthetics.spec.ts create mode 100644 packages/terraform-modules/src/pocket/PocketCloudwatchSynthetics.ts create mode 100644 packages/terraform-modules/src/pocket/PocketECSApplication.spec.ts create mode 100644 packages/terraform-modules/src/pocket/PocketECSApplication.ts create mode 100644 packages/terraform-modules/src/pocket/PocketECSCodePipeline.spec.ts create mode 100644 packages/terraform-modules/src/pocket/PocketECSCodePipeline.ts create mode 100644 packages/terraform-modules/src/pocket/PocketEventBridgeRuleWithMultipleTargets.spec.ts create mode 100644 packages/terraform-modules/src/pocket/PocketEventBridgeRuleWithMultipleTargets.ts create mode 100644 packages/terraform-modules/src/pocket/PocketEventBridgeWithLambdaTarget.spec.ts create mode 100644 packages/terraform-modules/src/pocket/PocketEventBridgeWithLambdaTarget.ts create mode 100644 packages/terraform-modules/src/pocket/PocketPagerDuty.spec.ts create mode 100644 packages/terraform-modules/src/pocket/PocketPagerDuty.ts create mode 100644 packages/terraform-modules/src/pocket/PocketSQSWithLambdaTarget.spec.ts create mode 100644 packages/terraform-modules/src/pocket/PocketSQSWithLambdaTarget.ts create mode 100644 packages/terraform-modules/src/pocket/PocketSynthetics.spec.ts create mode 100644 packages/terraform-modules/src/pocket/PocketSynthetics.ts create mode 100644 packages/terraform-modules/src/pocket/PocketVPC.spec.ts create mode 100644 packages/terraform-modules/src/pocket/PocketVPC.ts create mode 100644 packages/terraform-modules/src/pocket/PocketVersionedLambda.spec.ts create mode 100644 packages/terraform-modules/src/pocket/PocketVersionedLambda.ts create mode 100644 packages/terraform-modules/src/pocket/__snapshots__/PocketALBApplication.spec.ts.snap create mode 100644 packages/terraform-modules/src/pocket/__snapshots__/PocketApiGatewayLambdaIntegration.spec.ts.snap create mode 100644 packages/terraform-modules/src/pocket/__snapshots__/PocketCloudwatchSynthetics.spec.ts.snap create mode 100644 packages/terraform-modules/src/pocket/__snapshots__/PocketECSApplication.spec.ts.snap create mode 100644 packages/terraform-modules/src/pocket/__snapshots__/PocketECSCodePipeline.spec.ts.snap create mode 100644 packages/terraform-modules/src/pocket/__snapshots__/PocketEventBridgeRuleWithMultipleTargets.spec.ts.snap create mode 100644 packages/terraform-modules/src/pocket/__snapshots__/PocketEventBridgeWithLambdaTarget.spec.ts.snap create mode 100644 packages/terraform-modules/src/pocket/__snapshots__/PocketPagerDuty.spec.ts.snap create mode 100644 packages/terraform-modules/src/pocket/__snapshots__/PocketSQSWithLambdaTarget.spec.ts.snap create mode 100644 packages/terraform-modules/src/pocket/__snapshots__/PocketSynthetics.spec.ts.snap create mode 100644 packages/terraform-modules/src/pocket/__snapshots__/PocketVPC.spec.ts.snap create mode 100644 packages/terraform-modules/src/pocket/__snapshots__/PocketVersionedLambda.spec.ts.snap create mode 100644 packages/terraform-modules/src/testHelpers.ts create mode 100644 packages/terraform-modules/src/utilities.jest.ts create mode 100644 packages/terraform-modules/src/utilities.ts create mode 100644 packages/terraform-modules/tsconfig.json create mode 100644 packages/tsconfig/cdktf.json create mode 100644 packages/tsconfig/lambda.json create mode 100644 packages/tsconfig/library.json create mode 100644 servers/prospect-api/.eslintrc.js create mode 100644 servers/prospect-api/README.md create mode 100644 servers/prospect-api/jest.config.js create mode 100644 servers/prospect-api/jest.setup.js create mode 100644 servers/prospect-api/package.json create mode 100644 servers/prospect-api/schema.graphql create mode 100644 servers/prospect-api/src/aws/dynamodb/lib.integration.ts create mode 100644 servers/prospect-api/src/aws/dynamodb/lib.ts create mode 100644 servers/prospect-api/src/aws/eventBridgeClient.ts create mode 100644 servers/prospect-api/src/config/index.ts create mode 100644 servers/prospect-api/src/context.ts create mode 100644 servers/prospect-api/src/events/events.spec.ts create mode 100644 servers/prospect-api/src/events/events.ts create mode 100644 servers/prospect-api/src/events/snowplow-test-helpers.ts create mode 100644 servers/prospect-api/src/events/snowplow.integration.ts create mode 100644 servers/prospect-api/src/events/snowplow.ts create mode 100644 servers/prospect-api/src/events/types.ts create mode 100644 servers/prospect-api/src/express.ts create mode 100644 servers/prospect-api/src/lib.spec.ts create mode 100644 servers/prospect-api/src/lib.ts create mode 100644 servers/prospect-api/src/main.ts create mode 100644 servers/prospect-api/src/resolvers.ts create mode 100644 servers/prospect-api/src/seeder.ts create mode 100644 servers/prospect-api/src/server.ts create mode 100644 servers/prospect-api/src/test/admin-server/fragments.gql.ts create mode 100644 servers/prospect-api/src/test/admin-server/index.ts create mode 100644 servers/prospect-api/src/test/admin-server/mutations.gql.ts create mode 100644 servers/prospect-api/src/test/admin-server/queries.gql.ts create mode 100644 servers/prospect-api/src/typeDefs.ts create mode 100644 servers/prospect-api/src/types.ts create mode 100644 servers/prospect-api/tsconfig.json diff --git a/.circleci/branch-filters.yml b/.circleci/branch-filters.yml new file mode 100644 index 00000000..c0660c25 --- /dev/null +++ b/.circleci/branch-filters.yml @@ -0,0 +1,45 @@ +# Workflow shortcuts +# Anything in this file will be added to all the yaml files, before CircleCI merges them. +# This is because yaml files need to be valid before merge and that means things like anchors must be defined in each file. + +node_image: &node_image cimg/node:18.18.0 +node_version: &node_version 18.18.0 +base_image: &base_image cimg/base:2023.12 + +not_main: ¬_main + filters: + branches: + ignore: + - main + +only_main: &only_main + filters: + branches: + only: + - main + +not_dev: ¬_dev + filters: + branches: + ignore: + - dev + +only_dev_main: &only_dev_main + filters: + branches: + only: + - dev + - main + +not_dev_main: ¬_dev_main + filters: + branches: + ignore: + - dev + - main + +only_dev: &only_dev + filters: + branches: + only: + - dev \ No newline at end of file diff --git a/.circleci/config.yml b/.circleci/config.yml index fb55b104..b3907001 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -17,13 +17,13 @@ jobs: name: Generate List of Configs to Merge command: | # Generate a list of all the circleci configs we want to run - file_list=(".circleci/common.yml" ".circleci/repo-jobs.yml" ".circleci/prospect-api.yml") + file_list=(".circleci/shared.yml" ".circleci/monorepo-jobs.yml" ".circleci/prospect-api.yml") touch /tmp/configs.txt # Add file header to each file and dump it to the config list for file in "${file_list[@]}"; do echo "$file" >> /tmp/configs.txt - awk -v new_content="$(cat .circleci/header.yml)" 'BEGIN {print new_content} {print}' "$file" > temp_file && mv temp_file "$file" + awk -v new_content="$(cat .circleci/branch-filters.yml)" 'BEGIN {print new_content} {print}' "$file" > temp_file && mv temp_file "$file" done - path-filtering/generate-config: config-list-path: /tmp/configs.txt diff --git a/.circleci/monorepo-jobs.yml b/.circleci/monorepo-jobs.yml new file mode 100644 index 00000000..5aa6908f --- /dev/null +++ b/.circleci/monorepo-jobs.yml @@ -0,0 +1,34 @@ +jobs: + lint: + docker: + - image: *node_image + steps: + - checkout + - install_pnpm + - run: + name: Lint + # Need to build first, so the linter picks up all needed imports + command: | + pnpm run build + pnpm run lint --concurrency=2 + + test: + docker: + - image: *node_image + steps: + - checkout + - install_pnpm + - run: + name: Test + command: | + pnpm run test --concurrency=2 + +workflows: + monorepo: + jobs: + - lint: + <<: *not_dev_main + context: pocket + - test: + <<: *not_dev_main + context: pocket \ No newline at end of file diff --git a/.circleci/prospect-api.yml b/.circleci/prospect-api.yml new file mode 100644 index 00000000..f8a6c98c --- /dev/null +++ b/.circleci/prospect-api.yml @@ -0,0 +1,247 @@ + +workflows: + prospect-api: + jobs: + + ###### + # Every PR Jobs + ###### + - apollo: + name: prospect-api_apollo_admin + fed_graph_name: pocket-admin-api + schema_file_path: servers/prospect-api/schema.graphql + graph_name: prospect-api + prod_graph_url: https://prospect-api.readitlater.com + dev_graph_url: https://prospect-api.getpocket.dev + apollo_key_env: ADMIN_APOLLO_KEY + + - test_integrations: + <<: *not_dev_main + for: prospect_api + context: pocket + name: prospect-api_test_integrations + scope: prospect-api + + # Try building the ECS docker image on each branch + - build_image: + <<: *not_dev_main + context: pocket + for: prospect_api + name: prospect_api_build_docker + aws-access-key-id: Dev_AWS_ACCESS_KEY + aws-secret-access-key: Dev_AWS_SECRET_ACCESS_KEY + aws-region: Dev_AWS_DEFAULT_REGION + repo-name: prospectapi-dev-app + ecr-url: 410318598490.dkr.ecr.us-east-1.amazonaws.com + push: false + extra-build-args: --build-arg GIT_SHA=${CIRCLE_SHA1} --build-arg SCOPE=prospect-api --build-arg PORT=4026 + + - build_lambda: + <<: *not_dev_main + context: pocket + for: prospect_api + name: prospect-api_build_lambda + aws-access-key-id: Dev_AWS_ACCESS_KEY + aws-secret-access-key: Dev_AWS_SECRET_ACCESS_KEY + aws-region: Dev_AWS_DEFAULT_REGION + scope: prospect-api-aws_lambda + + - infrastructure: + <<: *not_dev_main + context: pocket + for: prospect_api + name: prospect-api_infrastructure_plan_prod + scope: prospect-api-cdk + stack-output-path: infrastructure/prospect-api/cdktf.out/stacks/prospect-api + resource-class: pocket/default-prod + workspace: Prod + dev: false + apply: false + + ###### + # Dev Branch Deployment (Dev Environment) + ###### + + - infrastructure: + <<: *only_dev + context: pocket + for: prospect_api + name: prospect-api_infrastructure_apply_dev + scope: prospect-api-cdk + stack-output-path: infrastructure/prospect-api/cdktf.out/stacks/prospect-api + resource-class: pocket/default-dev + workspace: Dev + apply: true + dev: true +# +# # Build & Deploy the Dev Docker Image + - build_image: + <<: *only_dev + context: pocket + for: prospect_api + name: prospect-api_build_docker_dev + aws-access-key-id: Dev_AWS_ACCESS_KEY + aws-secret-access-key: Dev_AWS_SECRET_ACCESS_KEY + aws-region: Dev_AWS_DEFAULT_REGION + repo-name: prospectapi-dev-app + ecr-url: 410318598490.dkr.ecr.us-east-1.amazonaws.com + push: true + extra-build-args: --build-arg GIT_SHA=${CIRCLE_SHA1} --build-arg SCOPE=prospect-api --build-arg PORT=4026 + requires: + - prospect-api_infrastructure_apply_dev + + - code_deploy_ecs: + <<: *only_dev + context: pocket + for: prospect_api + name: prospect-api_code_deploy_ecs_dev + resource-class: pocket/default-dev + codedeploy-app-name: ProspectAPI-Dev-ECS + codedeploy-group-name: ProspectAPI-Dev-ECS + requires: + - prospect-api_build_docker_dev + + - build_lambda: + <<: *only_dev + context: pocket + for: prospect_api + name: prospect-api_build_lambda_dev + aws-access-key-id: Dev_AWS_ACCESS_KEY + aws-secret-access-key: Dev_AWS_SECRET_ACCESS_KEY + aws-region: Dev_AWS_DEFAULT_REGION + scope: prospect-api-aws_lambda + s3-bucket: pocket-prospectapi-dev-sqs-translation + requires: + - prospect-api_infrastructure_apply_dev + + - code_deploy_lambda: + <<: *only_dev + context: pocket + for: prospect_api + name: prospect-api_code_deploy_lambda_dev + resource-class: pocket/default-dev + codedeploy-app-name: ProspectAPI-Dev-Sqs-Translation-Lambda + codedeploy-group-name: ProspectAPI-Dev-Sqs-Translation-Lambda + function-name: ProspectAPI-Dev-Sqs-Translation-Function + s3-bucket: pocket-prospectapi-dev-sqs-translation + requires: + - prospect-api_build_lambda_dev + + + # Notify sentry of dev deployment + - sentry_release_notification: + <<: *only_dev + name: prospect-api_sentry-release-notification-dev + context: pocket + for: prospect_api + sentry_project_name: prospect-api + sentry_env: development + sentry_org: pocket + requires: + - prospect-api_code_deploy_ecs_dev + + # Setup params we may use in Lambdas + - setup_deploy_params: + <<: *only_dev + name: prospect-api_deploy-params-dev + aws_access_key_id: Dev_AWS_ACCESS_KEY + aws_secret_access_key: Dev_AWS_SECRET_ACCESS_KEY + context: pocket + env: Dev + service_name: ProspectAPI + requires: + - prospect-api_code_deploy_ecs_dev + - prospect-api_code_deploy_lambda_dev + ###### + # Main Branch Deployment (Prod Environment) + ###### +# - infrastructure: +# <<: *only_main +# context: pocket +# for: prospect_api +# name: prospect-api_infrastructure_apply_prod +# scope: prospect-api-cdk +# stack-output-path: infrastructure/prospect-api/cdktf.out/stacks/prospect-api +# resource-class: pocket/default-prod +# workspace: Prod +# apply: true +# dev: false +# +# # Build & Deploy the Prod Docker Image +# - build_image: +# <<: *only_main +# context: pocket +# for: prospect_api +# name: prospect-api_build_docker_prod +# aws-access-key-id: Prod_AWS_ACCESS_KEY +# aws-secret-access-key: Prod_AWS_SECRET_ACCESS_KEY +# aws-region: Prod_AWS_DEFAULT_REGION +# repo-name: prospectapi-prod-app +# ecr-url: 996905175585.dkr.ecr.us-east-1.amazonaws.com +# push: true +# extra-build-args: --build-arg GIT_SHA=${CIRCLE_SHA1} --build-arg SCOPE=prospect-api --build-arg PORT=4026 +# requires: +# - prospect-api_infrastructure_apply_prod +# +# - code_deploy_ecs: +# <<: *only_main +# context: pocket +# for: prospect_api +# name: prospect-api_code_deploy_ecs_prod +# resource-class: pocket/default-prod +# codedeploy-app-name: ProspectAPI-Prod-ECS +# codedeploy-group-name: ProspectAPI-Prod-ECS +# requires: +# - prospect-api_build_docker_prod +# +# - build_lambda: +# <<: *only_main +# context: pocket +# for: prospect_api +# name: prospect-api_build_lambda_prod +# aws-access-key-id: Prod_AWS_ACCESS_KEY +# aws-secret-access-key: Prod_AWS_SECRET_ACCESS_KEY +# aws-region: Prod_AWS_DEFAULT_REGION +# scope: prospect-api-aws_lambda +# s3-bucket: pocket-prospectapi-prod-sqs-translation +# requires: +# - prospect-api_infrastructure_apply_prod +# +# - code_deploy_lambda: +# <<: *only_main +# context: pocket +# for: prospect_api +# name: prospect-api_code_deploy_lambda_prod +# resource-class: pocket/default-prod +# codedeploy-app-name: ProspectAPI-Prod-Sqs-Translation-Lambda +# codedeploy-group-name: ProspectAPI-Prod-Sqs-Translation-Lambda +# function-name: ProspectAPI-Prod-Sqs-Translation-Function +# s3-bucket: pocket-prospectapi-prod-sqs-translation +# requires: +# - prospect-api_build_lambda_prod +# +# # Notify sentry of main deployment +# - sentry_release_notification: +# <<: *only_main +# name: prospect-api_sentry-release-notification-prod +# context: pocket +# for: prospect_api +# sentry_project_name: prospect-api +# sentry_env: production +# sentry_org: pocket +# requires: +# - prospect-api_code_deploy_ecs_prod +# - prospect-api_code_deploy_lambda_prod +# +# # Setup params we may use in Lambdas +# - setup_deploy_params: +# <<: *only_main +# name: prospect-api_deploy-params-prod +# aws_access_key_id: Prod_AWS_ACCESS_KEY +# aws_secret_access_key: Prod_AWS_SECRET_ACCESS_KEY +# context: pocket +# env: Prod +# service_name: ProspectAPI +# requires: +# - prospect-api_code_deploy_ecs_prod +# - prospect-api_code_deploy_lambda_prod diff --git a/.circleci/scripts/setup.sh b/.circleci/scripts/setup.sh new file mode 100755 index 00000000..069bcb56 --- /dev/null +++ b/.circleci/scripts/setup.sh @@ -0,0 +1,13 @@ +#!/bin/bash + +set -e + +dir=$(dirname "$0") +while [[ "$1" ]]; do + case "$1" in + --aws) + "${dir}"/setup_aws.sh + ;; + esac + shift +done diff --git a/.circleci/scripts/setup_aws.sh b/.circleci/scripts/setup_aws.sh new file mode 100755 index 00000000..9ac7091b --- /dev/null +++ b/.circleci/scripts/setup_aws.sh @@ -0,0 +1,9 @@ +#!/bin/bash +set -e + +sudo apt-get update && sudo apt-get install -y python3-pip +pip3 install boto3 awscli-local awscli --no-build-isolation + +for Script in .docker/aws/*.sh ; do + bash "$Script" +done diff --git a/.circleci/shared.yml b/.circleci/shared.yml new file mode 100644 index 00000000..54aad918 --- /dev/null +++ b/.circleci/shared.yml @@ -0,0 +1,710 @@ +version: 2.1 + +orbs: + aws-cli: circleci/aws-cli@2.0.6 + aws-ecr: circleci/aws-ecr@7.3.0 + aws-code-deploy: circleci/aws-code-deploy@3.0.0 + jq: circleci/jq@2.2.0 + +# This is an enum that is used within all our jobs and our exit early job. +# As a new "service/deployment" is added you should add to the enum. +# Then each job you pass a "for" to, so that we can determine if this job is for this "commit" +repo_for_enum: &repo_for_enum + for: + description: which repo this job is relevant for + type: enum + enum: + - prospect_api + +resource_class_enmum: &resource_class_enmum + resource-class: + description: The self hosted runnner to run on + type: enum + enum: + - pocket/default-dev + - pocket/default-prod + + +parameters: + prospect_api: + type: boolean + default: false + +commands: + # Refrenced from https://github.com/kelvintaywl-cci/dynamic-config-showcase/blob/main/.circleci/next.yml + exit-early-if-irrelevant: + parameters: + <<: *repo_for_enum + steps: + - run: + name: stop early unless relevant + command: | + # looks up the relevant pipeline parameter via the env var + export RELEVANT=$(eval echo "\$<< parameters.for >>") + + # NOTE: env var values are strings (not boolean) + if [ "${RELEVANT}" = "1" ]; then + echo "continuing, since job is for << parameters.for >>" + else + echo "stopping early!" + circleci-agent step halt + fi + environment: + prospect_api: << pipeline.parameters.prospect_api >> + + install_pnpm: + steps: + - run: + name: Install pnpm package manager + command: | + sudo corepack enable + sudo corepack prepare pnpm@latest-8 --activate + - run: + name: Install Dependencies + command: | + pnpm install + install_infrastructure_pnpm: + steps: + - run: + name: Install and setup node + command: | + . /home/circleci/.codebuild_shims_wrapper.sh + nvm install + nvm use + npm install -g pnpm + pnpm install + install_codebuild_secrets: + steps: + - run: + name: Setup our secrets from AWS Secret Manager + command: | + . /home/circleci/.codebuild_shims_wrapper.sh + echo 'export SECRET_VALUE="$(aws secretsmanager get-secret-value --secret-id CodeBuild/Default --query SecretString --output text)"' >> "$BASH_ENV" + echo 'export TERRAFORM_TOKEN="$(echo $SECRET_VALUE | jq -r '.terraform_token')"' >> "$BASH_ENV" + echo 'export PAGERDUTY_TOKEN="$(echo $SECRET_VALUE | jq -r '.mozilla_pagerduty_token')"' >> "$BASH_ENV" + echo 'export GITHUB_ACCESS_TOKEN="$(echo $SECRET_VALUE | jq -r '.github_access_token')"' >> "$BASH_ENV" + echo 'export GITHUB_TOKEN="$(echo $SECRET_VALUE | jq -r '.github_access_token')"' >> "$BASH_ENV" + - run: + name: Save off terraform token + command: | + echo Setting Up Terraform Token + rc="credentials \"app.terraform.io\" { " + rc="${rc} token=\"$TERRAFORM_TOKEN\" " + rc="${rc}}" + echo "$rc" > ~/.terraformrc + +jobs: + + infrastructure: + description: Build and optionally deploy the infratructure + parameters: + scope: + description: The pnpm scope to build for + type: string + stack-output-path: + description: The pnpm output path + type: string + apply: + description: If you should apply + type: boolean + default: false + dev: + description: Whether or not its a dev build + type: boolean + default: false + workspace: + description: The terraform workspace + type: string + save_app_spec: + description: Whether or not we should save off the app spec file for later use + type: boolean + default: true + <<: [*repo_for_enum, *resource_class_enmum] + # Our self hosted runners dont support docker images, cause its not deployed in kubernetes, so we have some special steps + machine: true + resource_class: << parameters.resource-class >> + steps: + - exit-early-if-irrelevant: + for: << parameters.for >> + - checkout + - restore_cache: + name: Restore Tfenv + keys: + - tfenv-v2 + - run: + name: Install tfcmt + # make tfcmt executable for all users + command: | + curl -L https://github.com/suzuki-shunsuke/tfcmt/releases/download/v4.7.3/tfcmt_linux_amd64.tar.gz | tar xvzf - tfcmt + mv tfcmt /home/circleci/tfcmt + chmod a+x /home/circleci/tfcmt + - install_infrastructure_pnpm + - install_codebuild_secrets + - when: + condition: <> + steps: + - run: + name: Build Dev Infra + command: | + . /home/circleci/.codebuild_shims_wrapper.sh + nvm use + export NODE_ENV=development + pnpm run --filter=<< parameters.scope >>... build + pnpm run --filter=<< parameters.scope >> synth + - unless: + condition: <> + steps: + - run: + name: Build Prod Infra + command: | + . /home/circleci/.codebuild_shims_wrapper.sh + nvm use + export NODE_ENV=production + pnpm run --filter=<< parameters.scope >>... build + pnpm run --filter=<< parameters.scope >> synth + - run: + name: Setup terraform + command: | + . /home/circleci/.codebuild_shims_wrapper.sh + cd << parameters.stack-output-path >> + tfenv use + terraform init + - when: + condition: <> + steps: + - restore_cache: + name: Restore AppSpec Cache + keys: + - appspec-<< parameters.for >> + - when: + condition: <> + steps: + - attach_workspace: + at: /tmp/workspace + - run: + name: Terraform apply + command: | + . /home/circleci/.codebuild_shims_wrapper.sh + cd << parameters.stack-output-path >> + /home/circleci/tfcmt --var target:<< parameters.scope >><<#parameters.dev>>-dev<> apply -- terraform apply -auto-approve + - when: + condition: <> + steps: + - run: + name: Copy App Spec + command: | + . /home/circleci/.codebuild_shims_wrapper.sh + cd << parameters.stack-output-path >> + cp appspec.json /tmp/workspace/ + - persist_to_workspace: + root: /tmp/workspace + paths: + - 'appspec.json' + - unless: + condition: <> + steps: + - run: + name: Terraform plan + command: | + . /home/circleci/.codebuild_shims_wrapper.sh + cd << parameters.stack-output-path >> + /home/circleci/tfcmt --var target:<< parameters.scope >><<#parameters.dev>>-dev<> plan --skip-no-changes --patch -- terraform plan + - save_cache: + key: tfenv-v2 + paths: + - /home/circleci/.tfenv/versions/* + - /home/circleci/.tfenv/bin/terraform-* + - when: + condition: <> + steps: + - save_cache: + key: appspec-<< parameters.for >> + paths: + - << parameters.stack-output-path >>/appspec.json + code_deploy_ecs: + parameters: + workspace: + description: Workspace where the appspec.json are at + type: string + default: /tmp/workspace + codedeploy-app-name: + description: CodeDeploy app name + type: string + codedeploy-group-name: + description: CodeDeploy group name + type: string + <<: [*repo_for_enum, *resource_class_enmum] + # Our self hosted runners dont support docker images, cause its not deployed in kubernetes, so we have some special steps + machine: true + resource_class: << parameters.resource-class >> + steps: + - exit-early-if-irrelevant: + for: << parameters.for >> + - attach_workspace: + at: << parameters.workspace >> + - run: + name: CodeDeploy + command: | + . /home/circleci/.codebuild_shims_wrapper.sh + export AWS_PAGER="" + currentDeployment=$(aws deploy list-deployments --application-name "<< parameters.codedeploy-app-name >>" --deployment-group-name "<< parameters.codedeploy-group-name >>" --query "deployments[?status=='InProgress'].deploymentId" --output text --no-cli-pager) + if [ -n "$currentDeployment" ]; then + echo "There is already a deployment in progress with ID: $currentDeployment" waiting for it to finish + aws deploy wait deployment-successful --deployment-id "$deploymentId" --no-cli-pager || { + echo "Deployment failed or timed out." + exit 1 + } + fi + + export appspec=$(cat '<< parameters.workspace >>/appspec.json') + export REVISION="revisionType=AppSpecContent,appSpecContent={content='$appspec'}" + aws deploy \ + create-deployment \ + --application-name="<< parameters.codedeploy-app-name >>" \ + --deployment-group-name="<< parameters.codedeploy-group-name >>" \ + --description="Triggered from CircleCI" \ + --revision="$REVISION" \ + --no-cli-pager + + code_deploy_lambda: + parameters: + codedeploy-app-name: + description: CodeDeploy app name + type: string + codedeploy-group-name: + description: CodeDeploy group name + type: string + function-name: + description: > + The name of the Lambda Function to deploy to + type: string + s3-bucket: + type: string + description: The name of the bucket to deploy from + s3-key: + type: string + description: The name of the s3 key that contains the code to deploy + default: "" + function-alias: + type: string + description: The name of the lambda alias to use + default: DEPLOYED + <<: [*repo_for_enum, *resource_class_enmum] + # Our self hosted runners dont support docker images, cause its not deployed in kubernetes, so we have some special steps + machine: true + resource_class: << parameters.resource-class >> + steps: + - exit-early-if-irrelevant: + for: << parameters.for >> + - jq/install + - run: + name: Deploy Lambda + command: | + . /home/circleci/.codebuild_shims_wrapper.sh + export AWS_PAGER="" + aws lambda wait function-updated --function-name '<< parameters.function-name >>' + + s3Key="<< parameters.s3-key >>" + if [[ -z $s3Key ]]; then + s3Key="$CIRCLE_SHA1.zip" + fi + + aws lambda update-function-code \ + --function-name '<< parameters.function-name >>' \ + --s3-bucket '<< parameters.s3-bucket >>' \ + --s3-key "$s3Key" + + aws lambda wait function-updated --function-name '<< parameters.function-name >>' + + versionId=$(aws lambda publish-version \ + --function-name '<< parameters.function-name >>' | jq -r .Version) + + currentVersion=$(aws lambda get-alias \ + --function-name '<< parameters.function-name >>' \ + --name DEPLOYED | jq -r .FunctionVersion) + + app_spec_content_string="{'version':0.0,'Resources':[{'<< parameters.function-name >>':{'Type':'AWS::Lambda::Function','Properties':{'Name':'<< parameters.function-name >>','Alias':'<< parameters.function-alias >>','TargetVersion':'$versionId', 'CurrentVersion': '$currentVersion'}}}]}" + echo "$app_spec_content_string" + app_spec_content_sha256=$(echo -n "$app_spec_content_string" | shasum -a 256 | sed 's/ .*$//') + revision="revisionType=AppSpecContent,appSpecContent={content=\"$app_spec_content_string\",sha256=$app_spec_content_sha256}" + + aws lambda wait function-updated --function-name '<< parameters.function-name >>' + + aws deploy create-deployment \ + --application-name="<< parameters.codedeploy-app-name >>" \ + --deployment-group-name="<< parameters.codedeploy-group-name >>" \ + --description="Triggered build $CIRCLE_SHA1 from CircleCI" \ + --revision="$revision" + + test_integrations: + description: Run integration tests against external services, e.g. MySQL + parameters: + scope: + description: The pnpm scope to run tests for + type: string + <<: *repo_for_enum + docker: + - image: *node_image + auth: + username: $DOCKERHUB_USERNAME + password: $DOCKERHUB_PASSWORD + environment: + AWS_XRAY_LOG_LEVEL: silent + AWS_XRAY_CONTEXT_MISSING: LOG_ERROR + - image: mysql:8 + auth: + username: $DOCKERHUB_USERNAME + password: $DOCKERHUB_PASSWORD + environment: + - MYSQL_ALLOW_EMPTY_PASSWORD=yes + - TZ=UTC + command: --default_authentication_plugin=mysql_native_password --sql-mode="NO_ENGINE_SUBSTITUTION" --character-set-server=UTF8MB3 --collation-server=utf8_unicode_ci + - image: localstack/localstack:3.0.2 + auth: + username: $DOCKERHUB_USERNAME + password: $DOCKERHUB_PASSWORD + environment: + SERVICES: s3,dynamodb + - image: pocket/snowplow-micro:prod + auth: + username: $DOCKERHUB_USERNAME + password: $DOCKERHUB_PASSWORD + steps: + - exit-early-if-irrelevant: + for: << parameters.for >> + - checkout + - install_pnpm + - run: + name: run setup.sh + command: | + export $(egrep -v '^#' .docker/local.env | xargs -0) && ./.circleci/scripts/setup.sh --db --aws + - run: + # Note there is a bug in turbo repo requiring a build https://github.com/vercel/turbo/issues/1609 + name: run tests + command: | + export $(egrep -v '^#' .docker/local.env | xargs -0) + pnpm run --filter=<< parameters.scope >>... build + pnpm run --filter=<< parameters.scope >>... test-integrations -- --watchAll=false --forceExit --detectOpenHandles + + build_image: + description: Build and/or push docker image to ECR. + + parameters: + aws-access-key-id: + description: 'AWS access key id environment variable' + type: string + aws-region: + description: 'AWS region value' + type: string + aws-secret-access-key: + description: 'AWS secret access key environment variable' + type: string + ecr-url: + description: 'The ecr url' + type: string + extra-build-args: + description: 'Extra flags to pass to docker build. For examples, see https://docs.docker.com/engine/reference/commandline/build' + type: string + default: --build-arg GIT_SHA=${CIRCLE_SHA1} + push: + description: 'Whether or not to push the code' + type: boolean + default: false + repo-name: + description: 'The ecr repo name' + type: string + tag: + description: 'The docker tag name' + type: string + default: latest,$CIRCLE_SHA1 + <<: *repo_for_enum + executor: aws-cli/default + + steps: + - exit-early-if-irrelevant: + for: << parameters.for >> + - checkout + - aws-cli/setup: + aws-access-key-id: << parameters.aws-access-key-id >> + aws-secret-access-key: << parameters.aws-secret-access-key >> + aws-region: << parameters.aws-region >> + - run: + name: Setup common environment variables + command: | + { \ + echo 'export AWS_ECR_ACCOUNT_URL="<< parameters.ecr-url >>"'; \ + echo 'export REPO_NAME="<< parameters.repo-name >>"'; \ + } >> "$BASH_ENV" + - when: + condition: <> + steps: + - aws-ecr/build-and-push-image: + checkout: false + repo: << parameters.repo-name >> + setup-remote-docker: true + remote-docker-layer-caching: true + aws-access-key-id: << parameters.aws-access-key-id >> + aws-secret-access-key: << parameters.aws-secret-access-key >> + tag: << parameters.tag >> + extra-build-args: << parameters.extra-build-args >> + - unless: + condition: <> + steps: + - setup_remote_docker: + docker-layer-caching: true + - aws-ecr/build-image: + repo: << parameters.repo-name >> + tag: << parameters.tag >> + extra-build-args: << parameters.extra-build-args >> + + build_lambda: + description: Build and/or push lambda function. + parameters: + aws-access-key-id: + description: 'AWS access key id environment variable' + type: string + aws-region: + description: 'AWS region value' + type: string + aws-secret-access-key: + description: 'AWS secret access key environment variable' + type: string + s3-bucket: + description: 'The s3 bucket name' + type: string + default: "" + scope: + description: The pnpm scope to build for + type: string + <<: *repo_for_enum + docker: + - image: *node_image + auth: + username: $DOCKERHUB_USERNAME + password: $DOCKERHUB_PASSWORD + steps: + - exit-early-if-irrelevant: + for: << parameters.for >> + - run: + name: Setup Environment variables + command: | + echo "export SENTRY_AUTH_TOKEN="$SENTRY_BEARER"" >> "$BASH_ENV" + - checkout + - install_pnpm + - run: + # Theres a really annoying bug in PNPM deploy command that will try and create a folder at /home/pruned which we are not allowed to do, + # so we move it under 1 directory to let it do its thing. + # https://github.com/pnpm/pnpm/issues/5086 + # We also go crazy on the deploy command per https://github.com/pnpm/pnpm/issues/6166#issuecomment-1802541463 + name: Build lambda + command: | + pnpm install --filter=<< parameters.scope >>... --frozen-lockfile + pnpm run --filter=<< parameters.scope >>... build + mkdir -p ~/bug/project + cp -R . ~/bug/project/ + cd ~/bug/project/ + pnpm --config.shamefully-hoist=true --config.hoist=true --config.node-linker=true --config.symlinks=false --config.shared-workspace-lockfile=false deploy --filter=<< parameters.scope >> --prod pruned + - run: + name: Package Lambda + command: | + cd ~/bug/project/pruned + cp -r package.json dist/ + cp -r node_modules/ dist/node_modules/ + + cd dist + zip -r9 ~/project/${CIRCLE_SHA1}.zip . + mkdir /tmp/artifacts + cp ~/project/${CIRCLE_SHA1}.zip /tmp/artifacts/ + cd .. + maxFileSize=256000 # Get the size of the directory in kilobytes + export dirSize=$(du -s dist | cut -f1) + echo "Size is: $dirSize" + if ((dirSize > maxFileSize)); then + echo "Directory size is equal to or larger than $maxFileSize KB. which is the lambda limit" + exit 1 + fi + - when: + condition: << parameters.s3-bucket >> + steps: + - aws-cli/setup: + aws-access-key-id: << parameters.aws-access-key-id >> + aws-secret-access-key: << parameters.aws-secret-access-key >> + aws-region: << parameters.aws-region >> + - run: + name: Upload Package + command: aws s3 cp ${CIRCLE_SHA1}.zip s3://<< parameters.s3-bucket >>/ + - store_artifacts: + path: /tmp/artifacts + + apollo: + description: > + Runs Apollo rover schema check on the production graphql federated schema. + If it is the production branch will deploy the subgraph to the production federated graph. + If the branch is the development branch, will deploy the subgraph to the development federated graph. + + parameters: + fed_graph_name: + type: string + description: The name of federated graph to check + graph_name: + type: string + description: The name of this subgraph + schema_file_path: + type: string + description: The patht to the schema file + default: ./schema.graphql + prod_graph_url: + type: string + description: The production subgraph url + dev_graph_url: + type: string + description: The development subgraph url + prod_graph_variant_name: + type: string + description: The production variant graph name + default: "current" + dev_graph_variant_name: + type: string + description: The development variant graph name + default: "development" + prod_branch: + type: string + description: The production git branch + default: "main" + dev_branch: + type: string + description: The development git branch + default: "dev" + apollo_key_env: + type: env_var_name + default: APOLLO_KEY + description: The environment variable name of the apollo key to user + build_command: + description: 'build command to use if we need to' + type: string + default: "" + + docker: + - image: *node_image + auth: + username: $DOCKER_LOGIN + password: $DOCKER_PASSWORD + + steps: + - checkout + - run: + name: install rover + command: | + # download and install Rover + curl -sSL https://rover.apollo.dev/nix/latest | sh + + # This allows the PATH changes to persist to the next `run` step + echo "export PATH=$HOME/.rover/bin:$PATH" >> "$BASH_ENV" + - when: + condition: << parameters.build_command >> + steps: + - install_pnpm + - run: + name: build schema + command: | + << parameters.build_command >> + - run: + name: check service + command: | + export APOLLO_KEY=$<< parameters.apollo_key_env >> + rover subgraph check << parameters.fed_graph_name >>@<< parameters.prod_graph_variant_name >> --schema << parameters.schema_file_path >> --name=<< parameters.graph_name >> + - when: + condition: + equal: [<< parameters.prod_branch >>, << pipeline.git.branch >>] + steps: + - run: + name: push service to prod + command: | + export APOLLO_KEY=$<< parameters.apollo_key_env >> + rover subgraph publish << parameters.fed_graph_name >>@<< parameters.prod_graph_variant_name >> --schema << parameters.schema_file_path >> --routing-url << parameters.prod_graph_url >> --name=<< parameters.graph_name >> + - when: + condition: + equal: [<< parameters.dev_branch >>, << pipeline.git.branch >>] + steps: + - run: + name: push service to dev + command: | + export APOLLO_KEY=$<< parameters.apollo_key_env >> + rover subgraph publish << parameters.fed_graph_name >>@<< parameters.dev_graph_variant_name >> --schema << parameters.schema_file_path >> --routing-url << parameters.dev_graph_url >> --name=<< parameters.graph_name >> + + sentry_release_notification: + description: Create new release in Sentry + resource_class: small + parameters: + sentry_project_name: + type: string + description: the Sentry project name + sentry_env: + type: string + default: Prod + description: Which environment the release is going to + sentry_org: + type: string + description: The sentry org + <<: *repo_for_enum + docker: + - image: getsentry/sentry-cli + auth: + username: $DOCKER_LOGIN + password: $DOCKER_PASSWORD + steps: + - exit-early-if-irrelevant: + for: << parameters.for >> + - run: + name: Setup Environment variables + command: | + echo "export SENTRY_AUTH_TOKEN="$SENTRY_BEARER"" >> "$BASH_ENV" + echo "export SENTRY_ORG=<< parameters.sentry_org >>" >> "$BASH_ENV" + echo "export SENTRY_PROJECT=<< parameters.sentry_project_name >>" >> "$BASH_ENV" + - run: + name: Sentry Release Notification + command: | + source "$BASH_ENV" + sentry-cli releases new "$CIRCLE_SHA1" + sentry-cli releases set-commits "$CIRCLE_SHA1" --commit "Pocket/content-monorepo@$CIRCLE_SHA1" + sentry-cli releases finalize "$CIRCLE_SHA1" + - run: + name: Sentry Deploy Notification + command: | + source "$BASH_ENV" + sentry-cli releases deploys "$CIRCLE_SHA1" new -e "<< parameters.sentry_env >>" + + setup_deploy_params: + description: Sets up the CircleCI variables in AWS using the service name and env that is passed + parameters: + service_name: + description: Service Name + type: string + env: + description: Environment of the service + type: string + aws_access_key_id: + type: env_var_name + default: AWS_ACCESS_KEY_ID + description: AWS access key id for IAM role + aws_secret_access_key: + type: env_var_name + default: AWS_SECRET_ACCESS_KEY + description: AWS secret key for IAM role + docker: + - image: amazon/aws-cli:latest + auth: + username: $DOCKER_LOGIN + password: $DOCKER_PASSWORD + environment: + TERM: xterm + + steps: + - run: + name: Put SSM Parameters + command: | + export AWS_ACCESS_KEY_ID="${<< parameters.aws_access_key_id >>}" + export AWS_SECRET_ACCESS_KEY="${<< parameters.aws_secret_access_key >>}" + aws ssm put-parameter --name "/<< parameters.service_name >>/CircleCI/<< parameters.env >>/BUILD_BRANCH" --type "SecureString" --value "${CIRCLE_BRANCH}" --overwrite + aws ssm put-parameter --name "/<< parameters.service_name >>/CircleCI/<< parameters.env >>/SERVICE_VERSION" --type "SecureString" --value "${CIRCLE_BUILD_NUM}" --overwrite + aws ssm put-parameter --name "/<< parameters.service_name >>/CircleCI/<< parameters.env >>/SERVICE_HASH" --type "SecureString" --value "${CIRCLE_SHA1}" --overwrite diff --git a/.docker/aws/dynamodb.sh b/.docker/aws/dynamodb.sh new file mode 100755 index 00000000..9f1b0b47 --- /dev/null +++ b/.docker/aws/dynamodb.sh @@ -0,0 +1,31 @@ +#!/bin/bash +#set -x +# +#apt-get install jq -y +# +#files=($(ls "$(dirname ${BASH_SOURCE[0]})/dynamodb/")) +# +#for json_file_name in "${files[@]}"; do +# file_path=$(dirname "${BASH_SOURCE[0]}")/dynamodb/${json_file_name} +# table_name=$(jq -r '.TableName' "$file_path") +# # start fresh and delete the table if it exists +# awslocal dynamodb delete-table --table-name ${table_name} || true +# awslocal dynamodb create-table --cli-input-json file://$file_path +#done +# +#set +x +#!/bin/bash +set -x + +# for now, only prospect api required dynamodb from content services +TABLE_DEFINITIONS=( + 'prospect_api_prospects' +) + +for json_file in "${TABLE_DEFINITIONS[@]}"; do + # start fresh and delete the table if it exists + awslocal dynamodb delete-table --table-name ${json_file} || true + awslocal dynamodb create-table --cli-input-json file://$(dirname "${BASH_SOURCE[0]}")/dynamodb/${json_file}.json +done + +set +x \ No newline at end of file diff --git a/.docker/localstack/dynamodb/prospect_api_prospects.json b/.docker/aws/dynamodb/prospect_api_prospects.json similarity index 100% rename from .docker/localstack/dynamodb/prospect_api_prospects.json rename to .docker/aws/dynamodb/prospect_api_prospects.json diff --git a/.docker/localstack/s3.sh b/.docker/aws/s3.sh old mode 100644 new mode 100755 similarity index 100% rename from .docker/localstack/s3.sh rename to .docker/aws/s3.sh diff --git a/.docker/local.env b/.docker/local.env new file mode 100644 index 00000000..f6dc9604 --- /dev/null +++ b/.docker/local.env @@ -0,0 +1,10 @@ +AWS_ACCESS_KEY_ID=localstack-fake-id +AWS_DEFAULT_REGION=us-east-1 +AWS_REGION=us-east-1 +PROSPECT_API_PROSPECTS_TABLE=PROAPI-local-Prospects +AWS_ENDPOINT=http://localhost:4566 +AWS_SECRET_ACCESS_KEY=localstack-fake-key +LOCALSTACK_API_KEY=another-fake-key +AWS_XRAY_CONTEXT_MISSING=LOG_ERROR +AWS_XRAY_LOG_LEVEL=silent +SNOWPLOW_ENDPOINT=localhost:9090 \ No newline at end of file diff --git a/.docker/localstack/dynamodb.sh b/.docker/localstack/dynamodb.sh deleted file mode 100644 index ddcda307..00000000 --- a/.docker/localstack/dynamodb.sh +++ /dev/null @@ -1,15 +0,0 @@ -#!/bin/bash -set -x - -# for now, only prospect api required dynamodb from content services -TABLE_DEFINITIONS=( - 'prospect_api_prospects' -) - -for json_file in "${TABLE_DEFINITIONS[@]}"; do - # start fresh and delete the table if it exists - awslocal dynamodb delete-table --table-name ${json_file} || true - awslocal dynamodb create-table --cli-input-json file://$(dirname "${BASH_SOURCE[0]}")/dynamodb/${json_file}.json -done - -set +x \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml index 35eb1ddf..7969bb57 100644 --- a/.idea/vcs.xml +++ b/.idea/vcs.xml @@ -2,5 +2,6 @@ + \ No newline at end of file diff --git a/.npmrc b/.npmrc index 4cd53fbb..cb993121 100644 --- a/.npmrc +++ b/.npmrc @@ -6,5 +6,4 @@ public-hoist-pattern[]='@types/node' public-hoist-pattern[]='tslib' public-hoist-pattern[]='*apollo*' public-hoist-pattern[]='*graphql*' -public-hoist-pattern[]='*sentry*' public-hoist-pattern[]='*prisma*' \ No newline at end of file diff --git a/.nvmrc b/.nvmrc index cdeab1f7..2f3e481d 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -21.5 \ No newline at end of file +18.18 \ No newline at end of file diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 00000000..28377a59 --- /dev/null +++ b/.prettierignore @@ -0,0 +1 @@ +*/dist/* \ No newline at end of file diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 00000000..4386032d --- /dev/null +++ b/.prettierrc @@ -0,0 +1,6 @@ +{ + "useTabs": false, + "tabWidth": 2, + "semi": true, + "singleQuote": true +} \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index f0297316..fe74d68e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -6,7 +6,7 @@ # Docker build step that creates our # base image used in all steps #---------------------------------------- -FROM node:21.5.0-alpine AS base +FROM node:18.18-alpine AS base ARG SCOPE ARG PORT @@ -74,7 +74,7 @@ RUN pnpm run build --filter=${SCOPE}... ## Installing only the dev dependencies after we used them to build RUN rm -rf node_modules/ && pnpm install --prod --filter=${SCOPE} --frozen-lockfile - +RUN pnpm --filter=$SCOPE --prod deploy pruned #---------------------------------------- # Docker build step that: diff --git a/README.md b/README.md index 9bb6d70d..5763cc88 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ The following is an outline of how the monorepo is structured: monorepo / .circleci / all shared jobs + specific configs per service - . aws / + infrastructure / infra per service .docker / shared resources here @@ -37,9 +37,12 @@ Purpose of some important files in the root of the monorepo: `turbo.json` - Configure behavior for `turbo` ## Build +First, create a local `.env` file and copy the contents of `example.env`. + To build all services: ``` cd content-monorepo +nvm use pnpm install pnpm build ``` @@ -68,8 +71,16 @@ cd servers/server1 npm run dev ``` +## DynamoDB +Prospect-api uses `dynamodb` as the db system. When running `docker compose up`, the `localstack` container executes a `dynamodb.sh` script where the prospect-api table +is created. +To seed the table with data, run the seeding script: +``` +pnpm db:seed +``` + ## Prisma -Some of the services use `prisma` as their ORM, and to setup & seed the tables, some tasks need to be run separately from `docker compose`. +Collection-api & curated-corput-api use `prisma` as their ORM, and to setup & seed the tables, some tasks need to be run separately from `docker compose`. ### Generate Prisma Typescript Types `pnpm db:generate-client` diff --git a/docker-compose.yml b/docker-compose.yml index f9e9f320..63aa6f9e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,6 +1,6 @@ version: '3' services: - ## Start Common Services + # Start Common Services mysql: image: mysql:8 ports: @@ -14,7 +14,7 @@ services: ports: - '4566:4566' volumes: - - ./.docker/localstack:/init/ready.d + - ./.docker/aws:/etc/localstack/init/ready.d environment: SERVICES: s3,dynamodb DATA_DIR: /tmp/localstack/data diff --git a/example.env b/example.env new file mode 100644 index 00000000..c24d8e36 --- /dev/null +++ b/example.env @@ -0,0 +1,10 @@ +AWS_ACCESS_KEY_ID=localstack-fake-id +AWS_DEFAULT_REGION=us-east-1 +AWS_REGION=us-east-1 +PROSPECT_API_PROSPECTS_TABLE=PROAPI-local-Prospects +AWS_ENDPOINT=http://localstack:4566 +AWS_SECRET_ACCESS_KEY=localstack-fake-key +LOCALSTACK_API_KEY=another-fake-key +AWS_XRAY_CONTEXT_MISSING=LOG_ERROR +AWS_XRAY_LOG_LEVEL=silent +SNOWPLOW_ENDPOINT=snowplow:9090 \ No newline at end of file diff --git a/infrastructure/prospect-api/.eslintrc.js b/infrastructure/prospect-api/.eslintrc.js new file mode 100644 index 00000000..b93ae700 --- /dev/null +++ b/infrastructure/prospect-api/.eslintrc.js @@ -0,0 +1,3 @@ +module.exports = { + extends: ['custom/cdktf'], +}; \ No newline at end of file diff --git a/infrastructure/prospect-api/.terraform-version b/infrastructure/prospect-api/.terraform-version new file mode 100644 index 00000000..ec70f755 --- /dev/null +++ b/infrastructure/prospect-api/.terraform-version @@ -0,0 +1 @@ +1.6.6 diff --git a/infrastructure/prospect-api/cdktf.json b/infrastructure/prospect-api/cdktf.json new file mode 100644 index 00000000..119b5e1c --- /dev/null +++ b/infrastructure/prospect-api/cdktf.json @@ -0,0 +1,5 @@ +{ + "language": "typescript", + "app": "npm run --silent compile && node dist/main.js", + "projectId": "81b5e3a2-9a21-40e1-a902-33d2cb594b56" +} diff --git a/infrastructure/prospect-api/package.json b/infrastructure/prospect-api/package.json new file mode 100644 index 00000000..186cb265 --- /dev/null +++ b/infrastructure/prospect-api/package.json @@ -0,0 +1,24 @@ +{ + "name": "prospect-api-cdk", + "version": "1.0.0", + "main": "dist/main.js", + "types": "dist/main.ts", + "license": "MPL-2.0", + "private": true, + "scripts": { + "build": "rm -rf dist && tsc", + "synth": "cdktf synth", + "compile": "tsc --pretty", + "watch": "tsc -w", + "test": "echo ok", + "lint": "eslint --fix-dry-run \"src/**/*.ts\"", + "format": "eslint --fix \"src/**/*.ts\"" + }, + "dependencies": { + "@pocket-tools/terraform-modules": "workspace:*" + }, + "devDependencies": { + "eslint-config-custom": "workspace:*", + "tsconfig": "workspace:*" + } +} diff --git a/infrastructure/prospect-api/src/config/index.ts b/infrastructure/prospect-api/src/config/index.ts new file mode 100644 index 00000000..cbff3b1d --- /dev/null +++ b/infrastructure/prospect-api/src/config/index.ts @@ -0,0 +1,54 @@ +const name = 'ProspectAPI'; +const isDev = process.env.NODE_ENV === 'development'; +const githubConnectionArn = isDev + ? 'arn:aws:codestar-connections:us-east-1:410318598490:connection/7426c139-1aa0-49e2-aabc-5aef11092032' + : 'arn:aws:codestar-connections:us-east-1:996905175585:connection/5fa5aa2b-a2d2-43e3-ab5a-72ececfc1870'; +const branch = isDev ? 'dev' : 'main'; + +let domain; +let environment; + +if (process.env.NODE_ENV === 'development') { + environment = 'Dev'; + domain = 'prospect-api.getpocket.dev'; +} else { + environment = 'Prod'; + domain = 'prospect-api.readitlater.com'; +} + +const snowplowEndpoint = isDev + ? 'com-getpocket-prod1.mini.snplow.net' + : 'com-getpocket-prod1.collector.snplow.net'; + +export const config = { + name, + isDev, + prefix: `${name}-${environment}`, + circleCIPrefix: `/${name}/CircleCI/${environment}`, + shortName: 'PROAPI', + environment, + domain, + codePipeline: { + githubConnectionArn, + repository: 'pocket/prospect-api', + branch, + }, + healthCheck: { + command: [ + 'CMD-SHELL', + 'curl -f http://localhost:4026/.well-known/apollo/server-health || exit 1', + ], + interval: 15, + retries: 3, + timeout: 5, + startPeriod: 0, + }, + envVars: { + eventBusName: `PocketEventBridge-${environment}-Shared-Event-Bus`, + snowplowEndpoint, + }, + tags: { + service: name, + environment, + }, +}; diff --git a/infrastructure/prospect-api/src/dynamodb.ts b/infrastructure/prospect-api/src/dynamodb.ts new file mode 100644 index 00000000..cdc7725d --- /dev/null +++ b/infrastructure/prospect-api/src/dynamodb.ts @@ -0,0 +1,62 @@ +import { Construct } from 'constructs'; +import { config } from './config'; +import { + ApplicationDynamoDBTable, + ApplicationDynamoDBTableCapacityMode, +} from '@pocket-tools/terraform-modules'; + +export class DynamoDB extends Construct { + public readonly prospectsTable: ApplicationDynamoDBTable; + + constructor(scope: Construct, name: string) { + super(scope, name); + this.prospectsTable = this.setupProspectsTable(); + } + + /** + * Sets up the dynamodb table where the prospects will live + * @private + */ + private setupProspectsTable() { + // note that this config is mirrored in .docker/localstack/dynamodb/ + // if config changes here, that file should also be updated + return new ApplicationDynamoDBTable(this, `prospects`, { + tags: config.tags, + prefix: `${config.shortName}-${config.environment}-Prospects`, + capacityMode: ApplicationDynamoDBTableCapacityMode.ON_DEMAND, + tableConfig: { + hashKey: 'id', + // writeCapacity: 5, + // readCapacity: 5, + attribute: [ + { + name: 'id', + type: 'S', + }, + { + // the scheduled surface the prospect is targeted towards, e.g. "NEW_TAB_EN_US", "POCKET_HITS_DE_DE" + name: 'scheduledSurfaceGuid', + type: 'S', + }, + { + name: 'prospectType', + type: 'S', + }, + ], + // we will be retrieving prospects based on `scheduledSurfaceGuid` + // grouping ("ranging") by prospectType comes in handy when replacing + // prospects from SQS messages + globalSecondaryIndex: [ + { + name: 'scheduledSurfaceGuid-prospectType', + hashKey: 'scheduledSurfaceGuid', + rangeKey: 'prospectType', + projectionType: 'ALL', + readCapacity: 5, + writeCapacity: 5, + }, + ], + }, + }); + } +} \ No newline at end of file diff --git a/infrastructure/prospect-api/src/main.ts b/infrastructure/prospect-api/src/main.ts new file mode 100644 index 00000000..b85924ac --- /dev/null +++ b/infrastructure/prospect-api/src/main.ts @@ -0,0 +1,310 @@ +import { Construct } from 'constructs'; +import { + App, + DataTerraformRemoteState, + RemoteBackend, + TerraformStack, + MigrateIds, + Aspects, +} from 'cdktf'; + +import { + PocketALBApplication, + PocketPagerDuty, + PocketVPC, +} from '@pocket-tools/terraform-modules'; + +import { AwsProvider } from '@cdktf/provider-aws/lib/provider'; +import { PagerdutyProvider } from '@cdktf/provider-pagerduty/lib/provider'; +import { NullProvider } from '@cdktf/provider-null/lib/provider'; +import { LocalProvider } from '@cdktf/provider-local/lib/provider'; +import { ArchiveProvider } from '@cdktf/provider-archive/lib/provider'; +import { DataAwsRegion } from '@cdktf/provider-aws/lib/data-aws-region'; +import { DataAwsCallerIdentity } from '@cdktf/provider-aws/lib/data-aws-caller-identity'; +import { S3Bucket } from '@cdktf/provider-aws/lib/s3-bucket'; +import { DataAwsSnsTopic } from '@cdktf/provider-aws/lib/data-aws-sns-topic'; +import { DataAwsKmsAlias } from '@cdktf/provider-aws/lib/data-aws-kms-alias'; +import { CloudwatchLogGroup } from '@cdktf/provider-aws/lib/cloudwatch-log-group'; + +import { config } from './config'; +import { DynamoDB } from './dynamodb'; +import { SqsLambda } from './sqsLambda'; + +class ProspectAPI extends TerraformStack { + constructor(scope: Construct, name: string) { + super(scope, name); + + new AwsProvider(this, 'aws', { region: 'us-east-1' }); + + new PagerdutyProvider(this, 'pagerduty_provider', { token: undefined }); + new NullProvider(this, 'null-provider'); + new LocalProvider(this, 'local-provider'); + new ArchiveProvider(this, 'archive-provider'); + + new RemoteBackend(this, { + hostname: 'app.terraform.io', + organization: 'Pocket', + workspaces: [{ name: `${config.name}-${config.environment}`, }], + }); + + new PocketVPC(this, 'pocket-vpc'); + const region = new DataAwsRegion(this, 'region'); + const caller = new DataAwsCallerIdentity(this, 'caller'); + const dynamodb = new DynamoDB(this, 'dynamodb'); + + new SqsLambda(this, 'translation-lambda', dynamodb.prospectsTable); + + this.createPocketAlbApplication({ + s3: this.createS3Bucket(), + pagerDuty: this.createPagerDuty(), + secretsManagerKmsAlias: this.getSecretsManagerKmsAlias(), + snsTopic: this.getCodeDeploySnsTopic(), + region, + caller, + dynamodb, + }); + + // Pre cdktf 0.17 ids were generated differently so we need to apply a migration aspect + // https://developer.hashicorp.com/terraform/cdktf/concepts/aspects + Aspects.of(this).add(new MigrateIds()); + } + + /** + * Create S3 bucket for image uploads + * @private + */ + private createS3Bucket() { + return new S3Bucket(this, 'image-uploads', { + bucket: `pocket-${config.prefix.toLowerCase()}-images`, + }); + } + + /** + * Get the sns topic for code deploy + * @private + */ + private getCodeDeploySnsTopic() { + return new DataAwsSnsTopic(this, 'backend_notifications', { + name: `Backend-${config.environment}-ChatBot`, + }); + } + + /** + * Get secrets manager kms alias + * @private + */ + private getSecretsManagerKmsAlias() { + return new DataAwsKmsAlias(this, 'kms_alias', { + name: 'alias/aws/secretsmanager', + }); + } + + /** + * Create PagerDuty service for alerts + * @private + */ + private createPagerDuty() { + const incidentManagement = new DataTerraformRemoteState( + this, + 'incident_management', + { + organization: 'Pocket', + workspaces: { + name: 'incident-management', + }, + } + ); + + return new PocketPagerDuty(this, 'pagerduty', { + prefix: config.prefix, + service: { + // This is a Tier 2 service and as such only raises non-critical alarms. + criticalEscalationPolicyId: incidentManagement + .get('policy_default_non_critical_id') + .toString(), + nonCriticalEscalationPolicyId: incidentManagement + .get('policy_default_non_critical_id') + .toString(), + }, + }); + } + + private createPocketAlbApplication(dependencies: { + s3: S3Bucket; + pagerDuty: PocketPagerDuty; + region: DataAwsRegion; + caller: DataAwsCallerIdentity; + secretsManagerKmsAlias: DataAwsKmsAlias; + snsTopic: DataAwsSnsTopic; + dynamodb: DynamoDB; + }): PocketALBApplication { + const { s3, region, caller, secretsManagerKmsAlias, snsTopic, dynamodb } = + dependencies; + + return new PocketALBApplication(this, 'application', { + internal: true, + prefix: config.prefix, + alb6CharacterPrefix: config.shortName, + tags: config.tags, + cdn: false, + domain: config.domain, + containerConfigs: [ + { + name: 'app', + portMappings: [ + { + hostPort: 4026, + containerPort: 4026, + }, + ], + healthCheck: config.healthCheck, + envVars: [ + { + name: 'NODE_ENV', + value: process.env.NODE_ENV, + }, + { + name: 'ENVIRONMENT', + value: process.env.NODE_ENV, // this gives us a nice lowercase production and development + }, + { + name: 'AWS_DYNAMODB_ENDPOINT_URL', + value: `https://dynamodb.${region.name}.amazonaws.com`, + }, + { + name: 'PROSPECT_API_PROSPECTS_TABLE', + value: dynamodb.prospectsTable.dynamodb.name, + }, + { + name: 'AWS_S3_BUCKET', + value: s3.id, + }, + { + name: 'SNOWPLOW_ENDPOINT', + value: config.envVars.snowplowEndpoint, + }, + ], + logGroup: this.createCustomLogGroup('app'), + logMultilinePattern: '^\\S.+', + secretEnvVars: [ + { + name: 'SENTRY_DSN', + valueFrom: `arn:aws:ssm:${region.name}:${caller.accountId}:parameter/${config.name}/${config.environment}/SENTRY_DSN`, + }, + ], + }, + ], + codeDeploy: { + useCodeDeploy: true, + useCodePipeline: false, + snsNotificationTopicArn: snsTopic.arn, + notifications: { + //only notify on failed deploys + notifyOnFailed: true, + notifyOnStarted: false, + notifyOnSucceeded: false, + }, + }, + exposedContainer: { + name: 'app', + port: 4026, + healthCheckPath: '/.well-known/apollo/server-health', + }, + ecsIamConfig: { + prefix: config.prefix, + taskExecutionRolePolicyStatements: [ + //This policy could probably go in the shared module in the future. + { + actions: ['secretsmanager:GetSecretValue', 'kms:Decrypt'], + resources: [ + `arn:aws:secretsmanager:${region.name}:${caller.accountId}:secret:Shared`, + `arn:aws:secretsmanager:${region.name}:${caller.accountId}:secret:Shared/*`, + secretsManagerKmsAlias.targetKeyArn, + `arn:aws:secretsmanager:${region.name}:${caller.accountId}:secret:${config.name}/${config.environment}`, + `arn:aws:secretsmanager:${region.name}:${caller.accountId}:secret:${config.name}/${config.environment}/*`, + `arn:aws:secretsmanager:${region.name}:${caller.accountId}:secret:${config.prefix}`, + `arn:aws:secretsmanager:${region.name}:${caller.accountId}:secret:${config.prefix}/*`, + ], + effect: 'Allow', + }, + //This policy could probably go in the shared module in the future. + { + actions: ['ssm:GetParameter*'], + resources: [ + `arn:aws:ssm:${region.name}:${caller.accountId}:parameter/${config.name}/${config.environment}`, + `arn:aws:ssm:${region.name}:${caller.accountId}:parameter/${config.name}/${config.environment}/*`, + ], + effect: 'Allow', + }, + ], + taskRolePolicyStatements: [ + { + actions: [ + 'dynamodb:BatchGet*', + 'dynamodb:DescribeTable', + 'dynamodb:Get*', + 'dynamodb:Query', + 'dynamodb:Scan', + 'dynamodb:UpdateItem', + ], + resources: [ + dynamodb.prospectsTable.dynamodb.arn, + `${dynamodb.prospectsTable.dynamodb.arn}/*`, + ], + effect: 'Allow', + }, + { + actions: ['s3:*'], + resources: [`arn:aws:s3:::${s3.id}`, `arn:aws:s3:::${s3.id}/*`], + effect: 'Allow', + }, + { + actions: ['events:PutEvents'], + resources: [ + `arn:aws:events:${region.name}:${caller.accountId}:event-bus/${config.envVars.eventBusName}`, + ], + effect: 'Allow', + }, + ], + taskExecutionDefaultAttachmentArn: + 'arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy', + }, + autoscalingConfig: { + targetMinCapacity: 2, + targetMaxCapacity: 10, + }, + alarms: { + //TODO: When you start using the service add the pagerduty arns as an action for !config.isDev `pagerDuty.snsCriticalAlarmTopic.arn` + http5xxErrorPercentage: { + threshold: 25, + evaluationPeriods: 4, + period: 300, + actions: config.isDev ? [] : [], + }, + }, + }); + } + /** + * Create Custom log group for ECS to share across task revisions + * @param containerName + * @private + */ + private createCustomLogGroup(containerName: string) { + const logGroup = new CloudwatchLogGroup( + this, + `${containerName}-log-group`, + { + name: `/Backend/${config.prefix}/ecs/${containerName}`, + retentionInDays: 90, + skipDestroy: true, + tags: config.tags, + } + ); + + return logGroup.name; + } +} + +const app = new App(); +new ProspectAPI(app, 'prospect-api'); +app.synth(); diff --git a/infrastructure/prospect-api/src/sqsLambda.ts b/infrastructure/prospect-api/src/sqsLambda.ts new file mode 100644 index 00000000..76b29337 --- /dev/null +++ b/infrastructure/prospect-api/src/sqsLambda.ts @@ -0,0 +1,101 @@ +import { Construct } from 'constructs'; +import { config } from './config'; +import { + ApplicationDynamoDBTable, + PocketVPC, +} from '@pocket-tools/terraform-modules'; +import { PocketSQSWithLambdaTarget } from '@pocket-tools/terraform-modules'; +import { LAMBDA_RUNTIMES } from '@pocket-tools/terraform-modules'; +import { DataAwsSsmParameter } from '@cdktf/provider-aws/lib/data-aws-ssm-parameter'; +import { PocketPagerDuty } from '@pocket-tools/terraform-modules'; + +export class SqsLambda extends Construct { + constructor( + scope: Construct, + private name: string, + prospectsTable: ApplicationDynamoDBTable, + pagerDuty?: PocketPagerDuty + ) { + super(scope, name); + + const vpc = new PocketVPC(this, 'pocket-shared-vpc'); + + const { sentryDsn, gitSha } = this.getEnvVariableValues(); + + new PocketSQSWithLambdaTarget(this, 'translation-sqs-lambda', { + name: `${config.prefix}-Sqs-Translation`, + // batch size is 1 so SQS doesn't get smart and try to combine them + // (a combined message will mean a skipped candidate set from ML) + batchSize: 1, + batchWindow: 60, + sqsQueue: { + maxReceiveCount: 3, + visibilityTimeoutSeconds: 300, + }, + lambda: { + runtime: LAMBDA_RUNTIMES.NODEJS14, + handler: 'index.handler', + timeout: 120, + memorySizeInMb: 512, + reservedConcurrencyLimit: 1, + executionPolicyStatements: [ + { + effect: 'Allow', + actions: [ + 'dynamodb:BatchWriteItem', + 'dynamodb:PutItem', + 'dynamodb:DescribeTable', + 'dynamodb:UpdateItem', + 'dynamodb:Query', + ], + resources: [ + prospectsTable.dynamodb.arn, + `${prospectsTable.dynamodb.arn}/*`, + ], + }, + ], + environment: { + PROSPECT_API_PROSPECTS_TABLE: prospectsTable.dynamodb.name, + SENTRY_DSN: sentryDsn, + GIT_SHA: gitSha, + ENVIRONMENT: + config.environment === 'Prod' ? 'production' : 'development', + }, + vpcConfig: { + securityGroupIds: vpc.internalSecurityGroups.ids, + subnetIds: vpc.privateSubnetIds, + }, + codeDeploy: { + region: vpc.region, + accountId: vpc.accountId, + }, + alarms: { + // TODO: set better alarm values + /* + errors: { + evaluationPeriods: 3, + period: 3600, // 1 hour + threshold: 20, + actions: config.isDev + ? [] + : [pagerDuty!.snsNonCriticalAlarmTopic.arn], + }, + */ + }, + }, + tags: config.tags, + }); + } + + private getEnvVariableValues() { + const sentryDsn = new DataAwsSsmParameter(this, 'sentry-dsn', { + name: `/${config.name}/${config.environment}/SENTRY_DSN`, + }); + + const serviceHash = new DataAwsSsmParameter(this, 'service-hash', { + name: `${config.circleCIPrefix}/SERVICE_HASH`, + }); + + return { sentryDsn: sentryDsn.value, gitSha: serviceHash.value }; + } +} diff --git a/infrastructure/prospect-api/tsconfig.json b/infrastructure/prospect-api/tsconfig.json new file mode 100644 index 00000000..6fea514e --- /dev/null +++ b/infrastructure/prospect-api/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "tsconfig/cdktf.json", + "compilerOptions": { + "outDir": "dist", + "rootDir": "src" + }, + "exclude": ["node_modules"], + "include": ["src/**/*.ts", "src/config"] +} diff --git a/lambdas/prospect-api-lambda/.eslintrc.js b/lambdas/prospect-api-lambda/.eslintrc.js new file mode 100644 index 00000000..e0e49bbc --- /dev/null +++ b/lambdas/prospect-api-lambda/.eslintrc.js @@ -0,0 +1,3 @@ +module.exports = { + extends: ['custom/lambda'], +}; \ No newline at end of file diff --git a/lambdas/prospect-api-lambda/jest.config.js b/lambdas/prospect-api-lambda/jest.config.js new file mode 100644 index 00000000..ae85a274 --- /dev/null +++ b/lambdas/prospect-api-lambda/jest.config.js @@ -0,0 +1,7 @@ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', + testMatch: ['**/?(*.)+(spec|integration).ts'], + testPathIgnorePatterns: ['/dist/'], + setupFiles: ['./jest.setup.js'], +}; diff --git a/lambdas/prospect-api-lambda/jest.setup.js b/lambdas/prospect-api-lambda/jest.setup.js new file mode 100644 index 00000000..5c24690b --- /dev/null +++ b/lambdas/prospect-api-lambda/jest.setup.js @@ -0,0 +1,3 @@ +// AWS_ENDPOINT is set in .docker/local.env +// this file is to provide a value when running tests outside of the container +process.env.AWS_ENDPOINT = process.env.AWS_ENDPOINT || 'http://localhost:4566'; diff --git a/lambdas/prospect-api-lambda/package.json b/lambdas/prospect-api-lambda/package.json new file mode 100644 index 00000000..6daacedd --- /dev/null +++ b/lambdas/prospect-api-lambda/package.json @@ -0,0 +1,39 @@ +{ + "name": "prospect-api-aws_lambda", + "version": "1.0.0", + "description": "", + "main": "dist/index.js", + "scripts": { + "build": "rm -rf dist && tsc", + "test": "jest \"\\.spec\\.ts\"", + "test-integrations": "jest \"\\.integration\\.ts\" --runInBand", + "lint": "eslint --fix-dry-run \"src/**/*.ts\"", + "format": "eslint --fix \"src/**/*.ts\"" + }, + "author": "", + "license": "ISC", + "dependencies": { + "@aws-sdk/client-dynamodb": "3.391.0", + "@aws-sdk/client-lambda": "3.391.0", + "@aws-sdk/lib-dynamodb": "3.391.0", + "@sentry/serverless": "6.19.7", + "prospectapi-common": "workspace:*", + "uuid": "8.3.2" + }, + "devDependencies": { + "tsconfig": "workspace:*", + "eslint-config-custom": "workspace:*", + "@types/aws-lambda": "8.10.119", + "@types/chai": "4.3.4", + "@types/chai-as-promised": "7.1.5", + "@types/jest": "28.1.7", + "chai": "4.3.7", + "chai-as-promised": "7.1.1", + "jest": "29.7.0", + "ts-jest": "29.1.1" + }, + "files": [ + "dist", + "package.json" + ] +} diff --git a/lambdas/prospect-api-lambda/src/config.ts b/lambdas/prospect-api-lambda/src/config.ts new file mode 100644 index 00000000..622dc4c6 --- /dev/null +++ b/lambdas/prospect-api-lambda/src/config.ts @@ -0,0 +1,21 @@ +const config = { + environment: process.env.ENVIRONMENT || 'development', + aws: { + localEndpoint: process.env.AWS_ENDPOINT, + region: process.env.AWS_DEFAULT_REGION || 'us-east-1', + dynamoDb: { + table: + process.env.PROSPECT_API_PROSPECTS_TABLE || 'PROAPI-local-Prospects', + maxBatchDelete: 25, // this is a dynamo-enforced limit + maxAgeBeforeDeletion: 30, // if a prospect has been around more than 30 minutes, it's ripe for deletion + }, + }, + sentry: { + // these values are inserted into the environment in + // .aws/src/sqsLambda.ts + dsn: process.env.SENTRY_DSN || '', + release: process.env.GIT_SHA || '', + }, +}; + +export default config; diff --git a/lambdas/prospect-api-lambda/src/dynamodb/lib.integration.ts b/lambdas/prospect-api-lambda/src/dynamodb/lib.integration.ts new file mode 100644 index 00000000..51258d7a --- /dev/null +++ b/lambdas/prospect-api-lambda/src/dynamodb/lib.integration.ts @@ -0,0 +1,314 @@ +import * as chai from 'chai'; +import chaiAsPromised = require('chai-as-promised'); + +import { + Prospect, + Topics, + ProspectType, + dbClient, + getProspectById, + insertProspect, + truncateDb, + toUnixTimestamp, +} from 'prospectapi-common'; + +import config from '../config'; +import { + getProspectsForDeletion, + batchDeleteProspects, + deleteOldProspects, +} from './lib'; + +// so we can expect on async functions +chai.use(chaiAsPromised); +const expect = chai.expect; + +describe('dynamodb', () => { + let prospect: Prospect; + + beforeEach(() => { + prospect = { + id: 'ou812', + prospectId: 'ou813', + scheduledSurfaceGuid: 'NEW_TAB_EN_US', + topic: Topics.GAMING, + prospectType: ProspectType.TIMESPENT, + url: 'https://getpocket.com', + saveCount: 1680, + rank: 93, + }; + }); + + afterEach(async () => { + await truncateDb(dbClient); + }); + + describe('getProspectById', () => { + it('should retrieve by a valid id', async () => { + await insertProspect(dbClient, prospect); + + const res = await getProspectById(dbClient, prospect.id); + + expect(res).not.to.be.undefined; + }); + }); + + describe('batchDeleteProspects', () => { + it('should delete a single prospect by id', async () => { + // insert a prospect + await insertProspect(dbClient, prospect); + + // delete it + await batchDeleteProspects(dbClient, [prospect.id]); + + // make sure it's gone + const res = await getProspectById(dbClient, prospect.id); + + expect(res).to.be.undefined; + }); + + it('should delete multiple prospects by id', async () => { + // insert some prospects + prospect.id = 'deleteTest'; + + for (let i = 0; i < 5; i++) { + prospect.id = `deleteTest${i}`; + + await insertProspect(dbClient, prospect); + } + + await batchDeleteProspects(dbClient, [ + 'deleteTest1', + 'deleteTest2', + 'deleteTest4', + 'deleteTest5', + ]); + + // make sure! + + // this one should be deleted + let res = await getProspectById(dbClient, 'deleteTest1'); + expect(res).to.be.undefined; + + // this one should still exist + res = await getProspectById(dbClient, 'deleteTest3'); + expect(res).not.to.be.undefined; + }); + + it(`should throw if trying to delete more than ${config.aws.dynamoDb.maxBatchDelete} items (dynamo limit)`, async () => { + // insert some prospects + prospect.id = 'batchDeleteTooBigTest'; + const prospectIds: string[] = []; + + // one too many! + for (let i = 0; i < config.aws.dynamoDb.maxBatchDelete + 1; i++) { + prospect.id += 1; + + await insertProspect(dbClient, prospect); + + prospectIds.push(prospect.id); + } + + await expect( + batchDeleteProspects(dbClient, prospectIds) + ).to.be.rejectedWith( + `cannot delete more than ${config.aws.dynamoDb.maxBatchDelete} dynamo items at once! you are trying to delete ${prospectIds.length}!` + ); + }); + }); + + describe('getProspectsForDeletion', () => { + it('should retrieve expected results for provided scheduledSurfaceGuid and prospectType', async () => { + // insert a bunch of prospects matching the new tab and prospect type we + // want to delete. some of these will be 'new' and should not be deleted, + // some will be old and should be deleted + const now = new Date(); + + // mock date of prospects added from the current batch of SQS messages + const aFewMinutesAgo = toUnixTimestamp( + new Date(now.valueOf() - 3 * 60000) + ); + + // mock date of prospects added from the previous SQS batch + const lastMetaflowRun = toUnixTimestamp( + new Date( + now.valueOf() - (config.aws.dynamoDb.maxAgeBeforeDeletion + 1) * 60000 + ) + ); + + // insert some EN_US/TIMESPENT prospects + for (let i = 0; i < 12; i++) { + prospect.id += 1; + + // half the prospects should have been just added, the other half + // from a previous metaflow run / SQS batch + prospect.createdAt = i % 2 ? aFewMinutesAgo : lastMetaflowRun; + + await insertProspect(dbClient, prospect); + } + + // insert some EN_US/SYNDICATED_NEW + prospect.prospectType = ProspectType.SYNDICATED_NEW; + + for (let i = 0; i < 5; i++) { + prospect.id += 1; + + // to make sure timeframe isn't the *only* factor in retrieving + // prospects for deleting, make sure half of these *do* satisfy the + // time requirement (but do not satisfy the ProspectType requirement) + prospect.createdAt = i % 2 ? aFewMinutesAgo : lastMetaflowRun; + + await insertProspect(dbClient, prospect); + } + + const res = await getProspectsForDeletion( + dbClient, + 'NEW_TAB_EN_US', + ProspectType.TIMESPENT + ); + + // only half of the EN_US/TIMESPENT prospects should be returned, as the + // other half are too new. none of the 5 EN_US/SYNDICATED_NEW prospects + // should be returned + expect(res.length).to.equal(6); + + // should only get EN_US prospects that are 'old' + for (let i = 0; i < res.length; i++) { + expect(res[i].scheduledSurfaceGuid).to.equal('NEW_TAB_EN_US'); + expect(res[i].prospectType).to.equal(ProspectType.TIMESPENT); + expect(res[i].createdAt).to.be.equal(lastMetaflowRun); + } + }); + }); + + describe('deleteOldProspects', () => { + it('should delete all old prospects by new tab and prospect type', async () => { + // insert a bunch of prospects matching the new tab and prospect type we + // want to delete. some of these will be 'new' and should not be deleted, + // some will be old and should be deleted + const now = new Date(); + + // mock date of prospects added from the current batch of SQS messages + const aFewMinutesAgo = toUnixTimestamp( + new Date(now.valueOf() - 3 * 60000) + ); + + // mock date of prospects added from the previous SQS batch + const lastMetaflowRun = toUnixTimestamp( + new Date( + now.valueOf() - (config.aws.dynamoDb.maxAgeBeforeDeletion + 1) * 60000 + ) + ); + + let prospectCreatedAt; + + // ids need to be unique! set this before the for loop + prospect.id = 'shouldBeDeleted'; + + // these will all be EN_US/TIMESPENT + for (let i = 0; i < 12; i++) { + // so we can test the time limit on the delete, half the prospects + // should have been just added, the other half from a previous metaflow + // run / SQS batch + if (i % 2) { + prospectCreatedAt = aFewMinutesAgo; + } else { + prospectCreatedAt = lastMetaflowRun; + } + + // fake a set of prospects returned from dynamo + await insertProspect( + dbClient, + Object.assign({}, prospect, { + id: prospect.id + i, + createdAt: prospectCreatedAt, + }) + ); + } + + // insert some prospects not matching the new tab/prospect type to verify + // we aren't deleting too much data + + // ids need to be unique! set this before the for loop + prospect.id = 'shouldBeRetained'; + + // these should all be EN_US/SYNDICATED_NEW + for (let i = 0; i < 12; i++) { + // half the prospects should have been just added, the other half + // from a previous metaflow run / SQS batch + if (i % 2) { + prospectCreatedAt = aFewMinutesAgo; + } else { + prospectCreatedAt = lastMetaflowRun; + } + + // fake a set of prospects returned from dynamo + await insertProspect( + dbClient, + Object.assign({}, prospect, { + id: prospect.id + i, + // change the prospectType - this should mean they won't be + // deleted + prospectType: ProspectType.SYNDICATED_NEW, + createdAt: prospectCreatedAt, + }) + ); + } + + + // delete! + // this should delete 6 records + await deleteOldProspects( + dbClient, + 'NEW_TAB_EN_US', + ProspectType.TIMESPENT + ); + + // verify delete worked as expected + // we shouldn't get any results awaiting deletion + const deleteMeProspects = await getProspectsForDeletion( + dbClient, + 'NEW_TAB_EN_US', + ProspectType.TIMESPENT + ); + + expect(deleteMeProspects.length).to.equal(0); + }); + + it('should delete prospects in multiple batches', async () => { + // 52, meaning three "bathces" of deletes + const deletableCountToInsert = config.aws.dynamoDb.maxBatchDelete * 2 + 2; + const now = new Date(); + + // mock date of prospects added from an old, deletable SQS batch + const lastMetaflowRun = toUnixTimestamp( + new Date( + now.valueOf() - (config.aws.dynamoDb.maxAgeBeforeDeletion + 1) * 60000 + ) + ); + + // ids need to be unique! set this before the for loop + prospect.id = 'batchDeletes'; + + // insert more than 2 batch sizes of prospects + for (let i = 0; i < deletableCountToInsert; i++) { + // fake a set of prospects returned from dynamo + await insertProspect( + dbClient, + Object.assign({}, prospect, { + id: prospect.id + i, + createdAt: lastMetaflowRun, + }) + ); + } + + // delete! + // this should delete all records + await deleteOldProspects( + dbClient, + 'NEW_TAB_EN_US', + ProspectType.TIMESPENT + ); + }); + }); +}); diff --git a/lambdas/prospect-api-lambda/src/dynamodb/lib.ts b/lambdas/prospect-api-lambda/src/dynamodb/lib.ts new file mode 100644 index 00000000..a09a7c6e --- /dev/null +++ b/lambdas/prospect-api-lambda/src/dynamodb/lib.ts @@ -0,0 +1,161 @@ +import * as Sentry from '@sentry/serverless'; +import { + QueryCommand, + QueryCommandInput, + QueryCommandOutput, + BatchWriteCommand, + BatchWriteCommandInput, +} from '@aws-sdk/lib-dynamodb'; + +import { Prospect, ProspectType, toUnixTimestamp } from 'prospectapi-common'; + +import config from '../config'; + +/** + * retrieves prospects for the given scheduledSurface and prospectType. used + * when deleting outdated prospects - which happens each time an SQS message is + * processed. + * + * @param scheduledSurfaceGuid string GUID, e.g. 'NEW_TAB_EN_US' + * @param prospectType string GUID, e.g. 'GLOBAL' or 'TIMESPENT' + * @param maxAge number of minutes when a prospect is considered 'old' and + * ready for deletion + * @returns an array of Prospects filtered by scheduledSurface and prospectType + */ +export const getProspectsForDeletion = async ( + dbClient, + scheduledSurfaceGuid: string, + prospectType: ProspectType, + maxAge: number = config.aws.dynamoDb.maxAgeBeforeDeletion +): Promise => { + const now = new Date(); + const cutoffDate = toUnixTimestamp(new Date(now.valueOf() - maxAge * 60000)); + + const input: QueryCommandInput = { + TableName: config.aws.dynamoDb.table, + // this is our Global Secondary Index (GSI) + IndexName: 'scheduledSurfaceGuid-prospectType', + // because `prospectType` is the RANGE (meaning sort) key on the Global + // Secondary Index, we can refine results with the `and` clause + KeyConditionExpression: + 'scheduledSurfaceGuid = :scheduledSurfaceGuid and prospectType = :prospectType', + // note that FilterExpression happens *after* dynamo's 1MB result set + // limit. we shouldn't hit this limit based on the KeyCondtionExpression + // above - but we do log to sentry below if it happens + FilterExpression: 'createdAt <= :cutoffDate', + ExpressionAttributeValues: { + ':scheduledSurfaceGuid': scheduledSurfaceGuid, + ':prospectType': prospectType, + ':cutoffDate': cutoffDate, + }, + }; + + const res: QueryCommandOutput = await dbClient.send(new QueryCommand(input)); + + // LastEvaluatedKey will only be present if there are multiple pages of + // results from the query - which means we have more than 1MB of data for + // the given `scheduledSurfaceGuid` and `prospectType`. we do not expect this + // to ever happen, but we should be alerted in some way if it does. + if (res.LastEvaluatedKey) { + Sentry.captureMessage( + `method 'getProspectsByScheduledSurfaceGuidAndProspectType' called with '${scheduledSurfaceGuid}' and '${prospectType}' has multiple pages of results that we are not handling!` + ); + } + + if (res.Items?.length) { + return res.Items.map((item): Prospect => { + // force type safety + return { + id: item.id, + prospectId: item.prospectId, + scheduledSurfaceGuid: item.scheduledSurfaceGuid, + url: item.url, + prospectType: item.prospectType, + topic: item.topic, + saveCount: item.saveCount, + createdAt: item.createdAt, + curated: item.curated, + rank: item.rank, + }; + }); + } else { + return []; + } +}; + +/** + * deletes a batch of prospects by ids. maximum batch size is 25 (dynamo limit) + * + * @param prospectIds array of string ids + */ +export const batchDeleteProspects = async ( + dbClient, + prospectIds: string[] +): Promise => { + // basic check + if (prospectIds.length > config.aws.dynamoDb.maxBatchDelete) { + throw new Error( + `cannot delete more than ${config.aws.dynamoDb.maxBatchDelete} dynamo items at once! you are trying to delete ${prospectIds.length}!` + ); + } + + // dynamo syntax is...a thing, huh? + const writeRequests = prospectIds.map((prospectId: string) => { + return { + DeleteRequest: { + Key: { + // we can delete on id because it's our primary index + id: prospectId, + }, + }, + }; + }); + + const input: BatchWriteCommandInput = { + RequestItems: { + [config.aws.dynamoDb.table]: writeRequests, + }, + }; + + await dbClient.send(new BatchWriteCommand(input)); +}; + +/** + * deletes all old prospects matching the given scheduledSurfaceGuid and + * prospectType + * + * @param scheduledSurfaceGuid string guid of new tab, e.g. 'EN_US' + * @param prospectType ProspectType value + * + * @returns number of prospects deleted + */ +export const deleteOldProspects = async ( + dbClient, + scheduledSurfaceGuid: string, + prospectType: ProspectType +): Promise => { + // retrieve all prospects matching the scheduledSurfaceGuid and prospectType + const prospectsToDelete = await getProspectsForDeletion( + dbClient, + scheduledSurfaceGuid, + prospectType + ); + + let idsToDelete: string[] = []; + + // delete the prospects in batches + for (let i = 0; i < prospectsToDelete.length; i++) { + idsToDelete.push(prospectsToDelete[i].id); + + if (idsToDelete.length === config.aws.dynamoDb.maxBatchDelete) { + await batchDeleteProspects(dbClient, idsToDelete); + idsToDelete = []; + } + } + + if (idsToDelete.length) { + await batchDeleteProspects(dbClient, idsToDelete); + } + + return prospectsToDelete.length; +}; diff --git a/lambdas/prospect-api-lambda/src/index.ts b/lambdas/prospect-api-lambda/src/index.ts new file mode 100644 index 00000000..cabefb00 --- /dev/null +++ b/lambdas/prospect-api-lambda/src/index.ts @@ -0,0 +1,162 @@ +import { SQSEvent, SQSHandler } from 'aws-lambda'; +import * as Sentry from '@sentry/serverless'; + +import { + Prospect, + dbClient, + insertProspect, + deriveUrlMetadata, +} from 'prospectapi-common'; + +import config from './config'; +import { SqsProspect } from './types'; + +import { + getProspectsFromMessageJson, + convertSqsProspectToProspect, + hasValidStructure, + hydrateProspectMetadata, + validateProperties, +} from './lib'; + +import { deleteOldProspects } from './dynamodb/lib'; + +// little sentry initialization. no big deal. +Sentry.AWSLambda.init({ + dsn: config.sentry.dsn, + release: config.sentry.release, + environment: config.environment, +}); + +/** + * validates the SQS message, and, if valid, clears old dynamodb entries + * matching the ScheudledSurface (e.g. `NEW_TAB_EN_US`) and prospectType + * (e.g. `TIMESPENT`), and inserts the new prospects + * @param event data from an SQS message - should be an array of prospect + * objects + */ +const processor: SQSHandler = async (event: SQSEvent): Promise => { + console.log('raw event:'); + console.log(event); + + let json: any; + // this is raw data from SQS so we don't yet know if it conforms to the + // Prospect interface - hence `any` + let sqsProspects: Array = []; + let sqsProspect: SqsProspect; + let prospect: Prospect; + // store any error that may be encountered while processing for logging + let errors: string[] = []; + let prospectsProcessed = 0; + let prospectsErrored = 0; + + // is the SQS message valid JSON? + try { + // this is the (odd?) format of an SQS message + json = JSON.parse(event.Records[0].body); + + // we encountered some weird SQS behavior where multiple prospect messages + // were coming in. this was during an aws incident so may have been a fluke + // but - let's log to sentry if this happens again just in case. + + // if the above json conversion succeeds, this check should also succeed + // (as `Records` is verified to be an array). + if (event.Records.length > 1) { + Sentry.captureMessage('multiple records found in SQS message'); + } + } catch (e) { + Sentry.captureException( + 'invalid data provided / sqs event.Records[0].body is not valid JSON.' + ); + + // no need to do any more processing + return; + } + + // does the SQS JSON contain the expected data? + sqsProspects = getProspectsFromMessageJson(json); + + if (!sqsProspects) { + Sentry.captureException( + 'no `candidates` property exists on the SQS JSON, or `candidates` is not an array.' + ); + + // we don't have any prospects, so we're done + return; + } + + const idsProcessed = []; + + for (let i = 0; i < sqsProspects.length; i++) { + // save a local copy for easier reference + sqsProspect = sqsProspects[i]; + + // validate all necessary data points are present + if (hasValidStructure(sqsProspect)) { + // if the prospect has a valid structure, validate the property values + // for each invalid property, an error message will be added to the + // `errors` array + errors = validateProperties(sqsProspect); + } else { + errors.push('prospect is missing one or more properties.'); + } + + // if there are no errors, we are good to delete old prospects and insert + // the new ones + if (!errors.length) { + // convert Sqs formatted data to our standard format + prospect = convertSqsProspectToProspect(sqsProspect); + + // on the first pass through the loop only, delete all old prospects + // of the same scheduled surface and prospect type + if (i === 0) { + const deletedCount = await deleteOldProspects( + dbClient, + prospect.scheduledSurfaceGuid, + prospect.prospectType + ); + + console.log( + `deleted ${deletedCount} prospects for ${prospect.scheduledSurfaceGuid} / ${prospect.prospectType}` + ); + } + console.log(`Getting url metadata for ${prospect.url}`); + const urlMetadata = await deriveUrlMetadata(prospect.url); + console.log(`Got url metadata for ${prospect.url}`); + + prospect = hydrateProspectMetadata(prospect, urlMetadata); + + await insertProspect(dbClient, prospect); + + // TODO: should we keep this in prod? matt cooper was unaware this was + // happening, so it may be good to keep a record. we don't need to error + // here - dynamo will silently replace the existing entry. + if (idsProcessed.includes(prospect.id)) { + Sentry.captureMessage( + `${prospect.id} is a duplicate in this ${prospect.scheduledSurfaceGuid} / ${prospect.prospectType} batch!` + ); + } + + idsProcessed.push(prospect.id); + + prospectsProcessed++; + } else { + console.log('FAILED PARSING PROSPECT'); + console.log(`prospect: ${JSON.stringify(sqsProspect)}`); + console.log(`errors: ${errors}`); + + Sentry.captureException(errors.join(' | ')); + + prospectsErrored++; + } + + // reset errors array for next loop iteration + errors = []; + } + + console.log(`${prospectsProcessed} prospects inserted into dynamo.`); + console.log(`${prospectsErrored} prospects had errors.`); +}; + +// the actual function has to be wrapped in order for sentry to work +export const handler = Sentry.AWSLambda.wrapHandler(processor); diff --git a/lambdas/prospect-api-lambda/src/lib.spec.ts b/lambdas/prospect-api-lambda/src/lib.spec.ts new file mode 100644 index 00000000..113beb25 --- /dev/null +++ b/lambdas/prospect-api-lambda/src/lib.spec.ts @@ -0,0 +1,379 @@ +import { expect } from 'chai'; + +import { + Prospect, + ProspectType, + Topics, + UrlMetadata, +} from 'prospectapi-common'; + +import { + hasValidStructure, + hasValidProspectId, + hasValidScheduledSurfaceGuid, + hasValidPredictedTopic, + hasValidProspectSource, + hasValidUrl, + validateProperties, + convertSqsProspectToProspect, + hasValidSaveCount, + hydrateProspectMetadata, + getProspectsFromMessageJson, +} from './lib'; + +describe('lib', () => { + let validSqsProspect; + + beforeEach(() => { + validSqsProspect = { + prospect_id: '123abc', + scheduled_surface_guid: 'NEW_TAB_EN_US', + predicted_topic: Topics.ENTERTAINMENT, + prospect_source: ProspectType.SYNDICATED_NEW, + url: 'https://getpocket.com', + save_count: 1680, + rank: 1680, + }; + }); + + describe('hasValidStructure', () => { + it('should return true if object has all prospect properties', () => { + expect(hasValidStructure(validSqsProspect)).to.be.true; + }); + + it('should return false if object is missing a prospect_id', () => { + delete validSqsProspect.prospect_id; + + expect(hasValidStructure(validSqsProspect)).to.be.false; + }); + + it('should return false if object is missing a scheduled_surface_guid', () => { + delete validSqsProspect.scheduled_surface_guid; + + expect(hasValidStructure(validSqsProspect)).to.be.false; + }); + + it('should return false if object is missing a predicted_topic', () => { + delete validSqsProspect.predicted_topic; + + expect(hasValidStructure(validSqsProspect)).to.be.false; + }); + + it('should return false if object is missing a prospect_source', () => { + delete validSqsProspect.prospect_source; + + expect(hasValidStructure(validSqsProspect)).to.be.false; + }); + + it('should return false if object is missing a url', () => { + delete validSqsProspect.url; + + expect(hasValidStructure(validSqsProspect)).to.be.false; + }); + + it('should return false if object is missing a save_count', () => { + delete validSqsProspect.save_count; + + expect(hasValidStructure(validSqsProspect)).to.be.false; + }); + + it('should return false if object is missing a rank', () => { + delete validSqsProspect.rank; + + expect(hasValidStructure(validSqsProspect)).to.be.false; + }); + }); + + describe('hasValidProspectId', () => { + it('should return true if the prospect_id is a string', () => { + expect(hasValidProspectId(validSqsProspect)).to.be.true; + }); + + it('should return false if the prospect_id is not a string', () => { + validSqsProspect.prospect_id = 123; + expect(hasValidProspectId(validSqsProspect)).to.be.false; + }); + + it('should return false if the prospect_id is empty', () => { + validSqsProspect.prospect_id = undefined; + expect(hasValidProspectId(validSqsProspect)).to.be.false; + }); + }); + + describe('hasValidScheduledSurfaceGuid', () => { + it('should return true if the scheduled_surface_guid is one of our guids', () => { + expect(hasValidScheduledSurfaceGuid(validSqsProspect)).to.be.true; + }); + + it('should return false if the scheduled_surface_guid is not one of our guids', () => { + validSqsProspect.scheduled_surface_guid = 'NEW_TAB_CY_GB'; // Welsh + expect(hasValidScheduledSurfaceGuid(validSqsProspect)).to.be.false; + + validSqsProspect.scheduled_surface_guid = 42; + expect(hasValidScheduledSurfaceGuid(validSqsProspect)).to.be.false; + + validSqsProspect.scheduled_surface_guid = undefined; + expect(hasValidScheduledSurfaceGuid(validSqsProspect)).to.be.false; + }); + }); + + describe('hasValidPredictedTopic', () => { + it('should return true if the predicted_topic is one of our enums', () => { + expect(hasValidPredictedTopic(validSqsProspect)).to.be.true; + }); + + it('should return true if predicted_topic is an empty string', () => { + validSqsProspect.predicted_topic = ''; + expect(hasValidPredictedTopic(validSqsProspect)).to.be.true; + }); + + it('should return false if the predicted_topic is not one of our enums', () => { + validSqsProspect.predicted_topic = + 'companies without staging environments'; + expect(hasValidPredictedTopic(validSqsProspect)).to.be.false; + + validSqsProspect.predicted_topic = 900; + expect(hasValidPredictedTopic(validSqsProspect)).to.be.false; + + validSqsProspect.predicted_topic = undefined; + expect(hasValidPredictedTopic(validSqsProspect)).to.be.false; + }); + + it('should return false if predicted_topic is a single space string', () => { + validSqsProspect.predicted_topic = ' '; + expect(hasValidPredictedTopic(validSqsProspect)).to.be.false; + }); + }); + + describe('hasValidProspectSource', () => { + it("should return true if the prospect_source is one of our enums assigned to the prospect's scheduled surface", () => { + expect(hasValidProspectSource(validSqsProspect)).to.be.true; + }); + + it('should return false if the prospect_source is not one of our enums', () => { + validSqsProspect.prospect_source = 'GUT_FEELING'; + expect(hasValidProspectSource(validSqsProspect)).to.be.false; + }); + + it('should return true if the prospect_source is lower case', () => { + validSqsProspect.prospect_type = 'organic_timespent'; + expect(hasValidProspectSource(validSqsProspect)).to.be.true; + }); + + it('should return false if the prospect_source is not valid for the given scheduled surface', () => { + // de-DE does not have a SYNDICATED_NEW prospectType + validSqsProspect.scheduled_surface_guid = 'NEW_TAB_DE_DE'; + + expect(hasValidProspectSource(validSqsProspect)).to.be.false; + }); + }); + + describe('hasValidUrl', () => { + it('should return true if the url is valid', () => { + expect(hasValidUrl(validSqsProspect)).to.be.true; + }); + + it('should return false if the url is invalid', () => { + validSqsProspect.url = 'getpocket.com'; + expect(hasValidUrl(validSqsProspect)).to.be.false; + + validSqsProspect.url = 'ftp://getpocket.com'; + expect(hasValidUrl(validSqsProspect)).to.be.false; + + validSqsProspect.url = 'file://getpocket.com'; + expect(hasValidUrl(validSqsProspect)).to.be.false; + + validSqsProspect.url = 900; + expect(hasValidUrl(validSqsProspect)).to.be.false; + + validSqsProspect.url = undefined; + expect(hasValidUrl(validSqsProspect)).to.be.false; + }); + }); + + describe('hasValidSaveCount', () => { + it('should return true if the save_count is numeric', () => { + expect(hasValidSaveCount(validSqsProspect)).to.be.true; + }); + + it('should return false if the save_count is not numeric', () => { + validSqsProspect.save_count = '42'; + expect(hasValidSaveCount(validSqsProspect)).to.be.false; + + validSqsProspect.save_count = []; + expect(hasValidSaveCount(validSqsProspect)).to.be.false; + + validSqsProspect.save_count = {}; + expect(hasValidSaveCount(validSqsProspect)).to.be.false; + + validSqsProspect.save_count = false; + expect(hasValidSaveCount(validSqsProspect)).to.be.false; + }); + }); + + describe('validateProperties', () => { + it('should return a non-zero array if the prospect has errors', () => { + delete validSqsProspect.scheduled_surface_guid; + expect(validateProperties(validSqsProspect).length).to.be.greaterThan(0); + }); + + it('should return an empty array if the prospect is valid', () => { + expect(validateProperties(validSqsProspect).length).to.equal(0); + }); + }); + + describe('convertSqsProspectToProspect', () => { + it('should create a Prospect from an SqsProspect', () => { + const expected: Prospect = { + id: 'c3h5n3o9', + prospectId: validSqsProspect.prospect_id, + scheduledSurfaceGuid: validSqsProspect.scheduled_surface_guid, + url: validSqsProspect.url, + prospectType: validSqsProspect.prospect_source, + topic: validSqsProspect.predicted_topic, + saveCount: validSqsProspect.save_count, + rank: validSqsProspect.rank, + }; + + const result = convertSqsProspectToProspect(validSqsProspect); + + expect(result.id).to.exist; // we trust uuidV4 to work + expect(result.prospectId).to.equal(expected.prospectId); + expect(result.scheduledSurfaceGuid).to.equal( + expected.scheduledSurfaceGuid + ); + expect(result.url).to.equal(expected.url); + expect(result.prospectType).to.equal(expected.prospectType); + expect(result.topic).to.equal(expected.topic); + expect(result.saveCount).to.equal(expected.saveCount); + }); + }); + + describe('hydrateProspectMetaData', () => { + it('should hydrate the prospect with the url meta data fields ', () => { + const expected: Prospect = { + id: 'c3h5n3o9', + prospectId: validSqsProspect.prospect_id, + scheduledSurfaceGuid: validSqsProspect.scheduled_surface_guid, + url: validSqsProspect.url, + prospectType: validSqsProspect.prospect_source, + topic: validSqsProspect.predicted_topic, + saveCount: validSqsProspect.save_count, + rank: validSqsProspect.rank, + domain: 'test-domain', + excerpt: 'test-excerpt', + imageUrl: 'test-imageUrl', + language: 'en', + title: 'test-title', + publisher: 'test-publisher', + isCollection: false, + isSyndicated: true, + authors: 'questlove,rafael frumkin', + }; + + const prospectToHydrate = { + id: 'c3h5n3o9', + prospectId: validSqsProspect.prospect_id, + scheduledSurfaceGuid: validSqsProspect.scheduled_surface_guid, + url: validSqsProspect.url, + prospectType: validSqsProspect.prospect_source, + topic: validSqsProspect.predicted_topic, + saveCount: validSqsProspect.save_count, + rank: validSqsProspect.rank, + }; + + const urlMetadata: UrlMetadata = { + url: 'test-url', + domain: 'test-domain', + excerpt: 'test-excerpt', + imageUrl: 'test-imageUrl', + language: 'en', + title: 'test-title', + publisher: 'test-publisher', + isCollection: false, + isSyndicated: true, + authors: 'questlove,rafael frumkin', + }; + + expect(expected).to.deep.equal( + hydrateProspectMetadata(prospectToHydrate, urlMetadata) + ); + }); + + it('should hydrate prospect when parser has no metadata ', () => { + const expected: Prospect = { + id: 'c3h5n3o9', + prospectId: validSqsProspect.prospect_id, + scheduledSurfaceGuid: validSqsProspect.scheduled_surface_guid, + url: validSqsProspect.url, + prospectType: validSqsProspect.prospect_source, + topic: validSqsProspect.predicted_topic, + saveCount: validSqsProspect.save_count, + rank: validSqsProspect.rank, + domain: 'test-domain', + excerpt: undefined, + imageUrl: undefined, + language: undefined, + title: undefined, + publisher: undefined, + isCollection: undefined, + isSyndicated: undefined, + authors: undefined, + }; + + const prospectToHydrate = { + id: 'c3h5n3o9', + prospectId: validSqsProspect.prospect_id, + scheduledSurfaceGuid: validSqsProspect.scheduled_surface_guid, + url: validSqsProspect.url, + prospectType: validSqsProspect.prospect_source, + topic: validSqsProspect.predicted_topic, + saveCount: validSqsProspect.save_count, + rank: validSqsProspect.rank, + }; + + const urlMetadata: UrlMetadata = { + url: 'test-url', + domain: 'test-domain', + }; + + expect(expected).to.deep.equal( + hydrateProspectMetadata(prospectToHydrate, urlMetadata) + ); + }); + }); + + describe('getProspectsFromMessageJson', () => { + it('should get prospects from an event bridge > SQS formatted message', () => { + const json = { + foo: 'bar', + detail: { + candidates: [{ id: 1 }, { id: 2 }], + }, + }; + + expect(Array.isArray(getProspectsFromMessageJson(json))).to.be.true; + }); + + it('should return undefined if candidates is not an array', () => { + // event bridge > SQS + const json = { + foo: 'bar', + detail: { + candidates: { id: 1 }, + }, + }; + + expect(getProspectsFromMessageJson(json)).to.be.undefined; + }); + + it('should return undefined given an unexpected json structure', () => { + const json = { + foo: 'bar', + hammer: 'time', + }; + + expect(getProspectsFromMessageJson(json)).to.be.undefined; + }); + }); +}); diff --git a/lambdas/prospect-api-lambda/src/lib.ts b/lambdas/prospect-api-lambda/src/lib.ts new file mode 100644 index 00000000..007823d8 --- /dev/null +++ b/lambdas/prospect-api-lambda/src/lib.ts @@ -0,0 +1,250 @@ +import { URL } from 'url'; +import { v4 as uuidv4 } from 'uuid'; + +import { + Prospect, + ScheduledSurfaces, + Topics, + UrlMetadata, +} from 'prospectapi-common'; + +import { SqsProspect } from './types'; + +/** + * retrieves prospects from the sqs message + * + * @param messageBodyJson JSON in the `body` property of the sqs message + * @returns either the array of prospects in the JSON or undefined + */ +export const getProspectsFromMessageJson = ( + messageBodyJson: any +): { [key: string]: any }[] | undefined => { + if ( + messageBodyJson.detail?.candidates && + Array.isArray(messageBodyJson.detail.candidates) + ) { + return messageBodyJson.detail.candidates; + } else { + return undefined; + } +}; + +/** + * ensures prospect conforms to the interface + * + * @param prospect an object from an SQS message + * @returns boolean + */ +export const hasValidStructure = (prospect: unknown): prospect is Prospect => { + return ( + Object.prototype.hasOwnProperty.call(prospect, 'prospect_id') && + Object.prototype.hasOwnProperty.call(prospect, 'scheduled_surface_guid') && + Object.prototype.hasOwnProperty.call(prospect, 'predicted_topic') && + Object.prototype.hasOwnProperty.call(prospect, 'prospect_source') && + Object.prototype.hasOwnProperty.call(prospect, 'url') && + Object.prototype.hasOwnProperty.call(prospect, 'save_count') && + Object.prototype.hasOwnProperty.call(prospect, 'rank') + ); +}; + +/** + * ensure the `prospect_id` is a string + * @param sqsProspect a structurally valid prospect from sqs + * @returns boolean + */ +export const hasValidProspectId = (sqsProspect: SqsProspect): boolean => { + return typeof sqsProspect.prospect_id === 'string'; +}; + +/** + * ensure the `scheduled_surface_guid` is one of our pre-defined values + * + * @param sqsProspect a structurally valid prospect + * @returns boolean + */ +export const hasValidScheduledSurfaceGuid = ( + sqsProspect: SqsProspect +): boolean => { + let valid = false; + + for (let i = 0; i < ScheduledSurfaces.length; i++) { + if ( + typeof sqsProspect.scheduled_surface_guid === 'string' && + ScheduledSurfaces[i].guid === sqsProspect.scheduled_surface_guid + ) { + valid = true; + break; + } + } + + return valid; +}; + +/** + * ensure the `predicted_topic` is one of our pre-defined values or an empty string + * + * @param sqsProspect a structurally valid prospect + * @returns boolean + */ +export const hasValidPredictedTopic = (sqsProspect: SqsProspect): boolean => { + const prospectPredictedTopic = sqsProspect?.predicted_topic; + return prospectPredictedTopic + ? Object.values(Topics).includes(prospectPredictedTopic as any) + : prospectPredictedTopic === ''; +}; + +/** + * ensure the save_count is numeric + * @param sqsProspect a structurally valid prospect + * @returns boolean + */ +export const hasValidSaveCount = (sqsProspect: SqsProspect): boolean => { + return typeof sqsProspect.save_count === 'number'; +}; + +/** + * ensure the rank is numeric + * @param sqsProspect a structurally valid prospect + * @returns boolean + */ +export const hasValidRank = (sqsProspect: SqsProspect): boolean => { + return typeof sqsProspect.rank === 'number'; +}; + +/** + * ensure the prospect type is valid for the given feed id + * @param sqsProspect a structurally valid prospect + * @returns boolean + */ +export const hasValidProspectSource = (sqsProspect: SqsProspect): boolean => { + let isValid = false; + + for (let i = 0; i < ScheduledSurfaces.length; i++) { + // make sure the feed id is valid + if ( + typeof sqsProspect.scheduled_surface_guid === 'string' && + ScheduledSurfaces[i].guid === sqsProspect.scheduled_surface_guid + ) { + // make sure the prospect type is valid for the feed id + isValid = ScheduledSurfaces[i].prospectTypes.includes( + // we'll allow lowercase values. i guess. + sqsProspect.prospect_source.toUpperCase() as any + ); + break; + } + } + + return isValid; +}; + +/** + * ensure the url is valid and begins with http or https + * @param sqsProspect a structurally valid prospect + * @returns boolean + */ +export const hasValidUrl = (sqsProspect: SqsProspect): boolean => { + // https://stackoverflow.com/questions/5717093/check-if-a-javascript-string-is-a-url + let url; + + try { + url = new URL(sqsProspect.url); + } catch (_) { + return false; + } + + return url.protocol === 'http:' || url.protocol === 'https:'; +}; + +/** + * a helper function to return useful error messages while validating a + * prospect's properties + * @param sqsProspect Prospect + * @returns array of error messages + */ +export const validateProperties = (sqsProspect: SqsProspect): string[] => { + const errors: string[] = []; + + if (!hasValidProspectId(sqsProspect)) { + errors.push(`prospect_id '${sqsProspect.prospect_id} is not valid.`); + } + + if (!hasValidScheduledSurfaceGuid(sqsProspect)) { + errors.push( + `feed_name '${sqsProspect.scheduled_surface_guid} is not valid.` + ); + } + + if (!hasValidPredictedTopic(sqsProspect)) { + errors.push( + `predicted_topic '${sqsProspect.predicted_topic}' is not valid.` + ); + } + + if (!hasValidSaveCount(sqsProspect)) { + errors.push(`save_count '${sqsProspect.save_count}' is not numeric.`); + } + + if (!hasValidRank(sqsProspect)) { + errors.push(`rank '${sqsProspect.rank}' is not numeric.`); + } + + if (!hasValidProspectSource(sqsProspect)) { + errors.push( + `prospect_type '${sqsProspect.prospect_source}' is invalid for the feed_name '${sqsProspect.scheduled_surface_guid}'.` + ); + } + + if (!hasValidUrl(sqsProspect)) { + errors.push(`url '${sqsProspect.url} is not valid.`); + } + + return errors; +}; + +export const convertSqsProspectToProspect = ( + sqsProspect: SqsProspect +): Prospect => { + return { + id: uuidv4(), + prospectId: sqsProspect.prospect_id, + // make sure this matches our ALL CAPS guid value + scheduledSurfaceGuid: sqsProspect.scheduled_surface_guid.toUpperCase(), + prospectType: sqsProspect.prospect_source as any, + url: sqsProspect.url, + topic: sqsProspect.predicted_topic as any, + saveCount: sqsProspect.save_count, + rank: sqsProspect.rank, + }; +}; + +/** + * Populates a Prospect based off parser url meta data + * + * @param prospect a Prospect object from dynamo + * @param urlMetadata meta data from the parser + * @returns a Prospect object with metadata added + */ +export const hydrateProspectMetadata = ( + prospect: Prospect, + urlMetadata: UrlMetadata +): Prospect => { + // Mutating the function argument here to avoid creating + // more objects and be memory efficient + + // While the the urlMetaData and prospect fields match currently, + // they're not guaranteed to be the same in the future hence we're + // directly assigning them + + // NOTE: individual url metadata fields might be undefined + prospect.domain = urlMetadata.domain; + prospect.excerpt = urlMetadata.excerpt; + prospect.imageUrl = urlMetadata.imageUrl; + prospect.isCollection = urlMetadata.isCollection; + prospect.isSyndicated = urlMetadata.isSyndicated; + prospect.language = urlMetadata.language; + prospect.publisher = urlMetadata.publisher; + prospect.title = urlMetadata.title; + prospect.authors = urlMetadata.authors; + + return prospect; +}; diff --git a/lambdas/prospect-api-lambda/src/types.ts b/lambdas/prospect-api-lambda/src/types.ts new file mode 100644 index 00000000..99011ef6 --- /dev/null +++ b/lambdas/prospect-api-lambda/src/types.ts @@ -0,0 +1,10 @@ +// this is the raw data from metaflow/sqs +export interface SqsProspect { + prospect_id: string; + scheduled_surface_guid: string; + predicted_topic: string; + prospect_source: string; + url: string; + save_count: number; + rank: number; +} diff --git a/lambdas/prospect-api-lambda/tsconfig.json b/lambdas/prospect-api-lambda/tsconfig.json new file mode 100644 index 00000000..a91f81a0 --- /dev/null +++ b/lambdas/prospect-api-lambda/tsconfig.json @@ -0,0 +1,17 @@ +{ + "extends": "tsconfig/lambda.json", + "compilerOptions": { + "outDir": "dist", + "rootDir": "src" + }, + "exclude": [ + "node_modules", + "**/*.spec.ts", + "**/*.integration.ts", + "jest.config.js", + "jest.setup.js" + ], + "include": [ + "src/**/*.ts", + ] +} diff --git a/package.json b/package.json index c8f6ca61..9c0e9a7c 100644 --- a/package.json +++ b/package.json @@ -6,13 +6,18 @@ "preinstall": "npx only-allow pnpm", "build": "dotenv -- turbo run build", "dev": "dotenv -- turbo dev", - "db:generate-client": "turbo run db:generate-client", - "db:migrate:reset": "turbo run db:migrate:reset", + "synth": "dotenv -- turbo run synth", + "test": "dotenv -- turbo run test", + "test-integrations": "dotenv -- turbo run test-integrations --no-cache", + "db:seed": "dotenv -- turbo run db:seed", + "db:generate-client": "dotenv -- turbo run db:generate-client", + "db:migrate:reset": "dotenv -- turbo run db:migrate:reset", "lint": "turbo run lint", - "format": "turbo run format" + "format": "turbo run format", + "clean": "rm -rf .turbo node_modules packages/**/.turbo packages/**/dist packages/**/node_modules servers/**/logs servers/**/.turbo servers/**/dist servers/**/node_modules lambdas/**/logs lambdas/**/.turbo lambdas/**/dist lambdas/**/node_modules infrastructure/**/dist infrastructure/**/node_modules infrastructure/**/.turbo infrastructure/**/cdktf.out" }, "engines": { - "node": "^21.5" + "node": "18.18" }, "packageManager": "pnpm@8.12.1", "dependencies": { diff --git a/packages/eslint-config-custom/cdktf.js b/packages/eslint-config-custom/cdktf.js new file mode 100644 index 00000000..7ef09750 --- /dev/null +++ b/packages/eslint-config-custom/cdktf.js @@ -0,0 +1,50 @@ +module.exports = { + extends: [ + 'eslint:recommended', + 'plugin:@typescript-eslint/eslint-recommended', + 'plugin:@typescript-eslint/recommended', + 'plugin:prettier/recommended', + ], + plugins: [], + rules: { + 'prettier/prettier': [ + 'error', + { + useTabs: false, // \( ̄▽ ̄)/ + tabWidth: 2, + semi: true, + singleQuote: true, + }, + ], + // allows unused vars when declared in arguments + '@typescript-eslint/no-unused-vars': [ + 'error', + { vars: 'all', args: 'none' }, + ], + // disables case checks for class/interface/type + '@typescript-eslint/class-name-casing': 0, + // disables case checks for properties + '@typescript-eslint/camelcase': 0, + // allows 'any' typehint + '@typescript-eslint/no-explicit-any': 0, + // enforces 2 spaces indent + indent: [ + 'error', + 2, + { + SwitchCase: 1, + VariableDeclarator: { var: 2, let: 2, const: 3 }, + outerIIFEBody: 1, + MemberExpression: 1, + FunctionDeclaration: { parameters: 1, body: 1 }, + FunctionExpression: { parameters: 1, body: 1 }, + CallExpression: { arguments: 1 }, + ArrayExpression: 1, + ObjectExpression: 1, + ImportDeclaration: 1, + flatTernaryExpressions: false, + ignoreComments: false, + }, + ], + }, +}; diff --git a/packages/eslint-config-custom/lambda.js b/packages/eslint-config-custom/lambda.js new file mode 100644 index 00000000..8c383d62 --- /dev/null +++ b/packages/eslint-config-custom/lambda.js @@ -0,0 +1,31 @@ +module.exports = { + extends: [ + 'eslint:recommended', + 'plugin:@typescript-eslint/eslint-recommended', + 'plugin:@typescript-eslint/recommended', + 'plugin:prettier/recommended', + ], + plugins: [], + rules: { + 'prettier/prettier': [ + 'error', + { + useTabs: false, // \( ̄▽ ̄)/ + tabWidth: 2, + semi: true, + singleQuote: true, + }, + ], + // allows unused vars when declared in arguments + '@typescript-eslint/no-unused-vars': [ + 'error', + { vars: 'all', args: 'none' }, + ], + // disables case checks for class/interface/type + '@typescript-eslint/class-name-casing': 0, + // disables case checks for properties + '@typescript-eslint/camelcase': 0, + // allows 'any' typehint + '@typescript-eslint/no-explicit-any': 0, + }, +}; diff --git a/packages/eslint-config-custom/library.js b/packages/eslint-config-custom/library.js new file mode 100644 index 00000000..847c9463 --- /dev/null +++ b/packages/eslint-config-custom/library.js @@ -0,0 +1,53 @@ +module.exports = { + extends: [ + 'eslint:recommended', + 'plugin:@typescript-eslint/eslint-recommended', + 'plugin:@typescript-eslint/recommended', + 'plugin:prettier/recommended', + ], + // by default, eslint ignores directories beginning with a . + // the below tells eslint *not* to ignore the .aws directory + ignorePatterns: ['!/dist'], + plugins: [], + rules: { + 'prettier/prettier': [ + 'error', + { + useTabs: false, // \( ̄▽ ̄)/ + tabWidth: 2, + semi: true, + singleQuote: true, + }, + ], + // allows unused vars when declared in arguments + '@typescript-eslint/no-unused-vars': [ + 'error', + { vars: 'all', args: 'none' }, + ], + // disables case checks for class/interface/type + '@typescript-eslint/class-name-casing': 0, + // disables case checks for properties + '@typescript-eslint/camelcase': 0, + // allows 'any' typehint + '@typescript-eslint/no-explicit-any': 0, + // enforces 2 spaces indent + indent: [ + 'error', + 2, + { + SwitchCase: 1, + VariableDeclarator: { var: 2, let: 2, const: 3 }, + outerIIFEBody: 1, + MemberExpression: 1, + FunctionDeclaration: { parameters: 1, body: 1 }, + FunctionExpression: { parameters: 1, body: 1 }, + CallExpression: { arguments: 1 }, + ArrayExpression: 1, + ObjectExpression: 1, + ImportDeclaration: 1, + flatTernaryExpressions: false, + ignoreComments: false, + }, + ], + }, +}; diff --git a/packages/eslint-config-custom/react-internal.js b/packages/eslint-config-custom/react-internal.js index 05ff9bea..a12f9784 100644 --- a/packages/eslint-config-custom/react-internal.js +++ b/packages/eslint-config-custom/react-internal.js @@ -31,7 +31,7 @@ module.exports = { }, }, }, - ignorePatterns: ["node_modules/", "dist/", ".eslintrc.js"], + ignorePatterns: ["node_modules/", "dist/", "..eslintrc.js"], rules: { // add specific rules configurations here diff --git a/packages/prospectapi-common/.eslintrc.js b/packages/prospectapi-common/.eslintrc.js new file mode 100644 index 00000000..5e391b08 --- /dev/null +++ b/packages/prospectapi-common/.eslintrc.js @@ -0,0 +1,3 @@ +module.exports = { + extends: ['custom/library'], +}; diff --git a/packages/prospectapi-common/client-api-proxy.ts b/packages/prospectapi-common/client-api-proxy.ts new file mode 100644 index 00000000..839d8d3b --- /dev/null +++ b/packages/prospectapi-common/client-api-proxy.ts @@ -0,0 +1,89 @@ +import * as Sentry from '@sentry/node'; + +// we have to import from @apollo/client/core because importing from +// @apollo/client assumes react will be available and things fail. when things +// get to this state, we should take a long look in the mirror. +import { + ApolloClient, + InMemoryCache, + createHttpLink, +} from '@apollo/client/core'; +import fetch from 'cross-fetch'; +import gql from 'graphql-tag'; + +import config from './config'; +import { ClientApiItem } from './types'; + +let client; + +/** + * calls client API to get data for the given url + * @returns JSON response from client API + */ +export const getUrlMetadata = async ( + url: string, +): Promise => { + // Move Apollo Client instantiation to inside the function to prevent memory leaks. + if (!client) { + client = new ApolloClient({ + link: createHttpLink({ fetch, uri: config.app.clientApiEndpoint }), + cache: new InMemoryCache(), + name: config.app.apolloClientName, + version: config.app.version, + defaultOptions: { + // Disable cache on all queries to save on memory. + query: { + fetchPolicy: 'no-cache', + }, + // And let's not watch any future queries either! + watchQuery: { + fetchPolicy: 'no-cache', + }, + }, + }); + } + + const data = await client.query({ + query: gql` + query ProspectApiUrlMetadata($url: String!) { + itemByUrl(url: $url) { + resolvedUrl + excerpt + title + language + topImageUrl + authors { + name + } + domainMetadata { + name + } + syndicatedArticle { + authorNames + excerpt + mainImage + publisher { + name + url + } + title + } + collection { + slug + } + } + } + `, + variables: { + url, + }, + }); + + if (!data.data?.itemByUrl) { + Sentry.captureException(new Error(`no parser data found for url ${url}`)); + + return null; + } + + return data.data?.itemByUrl; +}; diff --git a/packages/prospectapi-common/config.ts b/packages/prospectapi-common/config.ts new file mode 100644 index 00000000..d0540345 --- /dev/null +++ b/packages/prospectapi-common/config.ts @@ -0,0 +1,22 @@ +export default { + environment: process.env.NODE_ENV || 'development', + app: { + apolloClientName: 'ProspectAPI', + clientApiEndpoint: 'https://client-api.getpocket.com', + version: `${process.env.GIT_SHA ?? 'local'}`, + }, + aws: { + localEndpoint: process.env.AWS_ENDPOINT, + region: process.env.AWS_DEFAULT_REGION || 'us-east-1', + dynamoDb: { + table: + process.env.PROSPECT_API_PROSPECTS_TABLE || 'PROAPI-local-Prospects', + }, + }, + // environment variables below are set in .aws/src/main.ts + sentry: { + dsn: process.env.SENTRY_DSN || '', + release: process.env.GIT_SHA || '', + environment: process.env.NODE_ENV || 'development', + }, +}; diff --git a/packages/prospectapi-common/dynamodb-client.ts b/packages/prospectapi-common/dynamodb-client.ts new file mode 100644 index 00000000..53c23069 --- /dev/null +++ b/packages/prospectapi-common/dynamodb-client.ts @@ -0,0 +1,33 @@ +import { DynamoDBClient } from '@aws-sdk/client-dynamodb'; +import { DynamoDBDocumentClient } from '@aws-sdk/lib-dynamodb'; + +import config from './config'; + +// base dynamo client +const dynamo = new DynamoDBClient({ + apiVersion: '2012-08-10', + endpoint: config.aws.localEndpoint, + region: config.aws.region, +}); + +// from the docs: +// https://www.npmjs.com/package/@aws-sdk/lib-dynamodb +// The document client simplifies working with items in Amazon DynamoDB by +// abstracting away the notion of attribute values. This abstraction annotates +// native JavaScript types supplied as input parameters, as well as converts +// annotated response data to native JavaScript types. +// +// basically this makes it much easier to work with dynamo +export const dbClient: DynamoDBDocumentClient = DynamoDBDocumentClient.from( + dynamo, + { + marshallOptions: { + // if any values of an object trying to be inserted are undefined, + // (as they may well be in regards to parser metadata) do not insert + // those values into the db. + // + // dynamo actually errors out if this is left as the default `false`. + removeUndefinedValues: true, + }, + }, +); diff --git a/packages/prospectapi-common/dynamodb.integration.ts b/packages/prospectapi-common/dynamodb.integration.ts new file mode 100644 index 00000000..7063c089 --- /dev/null +++ b/packages/prospectapi-common/dynamodb.integration.ts @@ -0,0 +1,33 @@ +import * as chai from 'chai'; + +import { ProspectType } from './types'; +import { dbClient } from './dynamodb-client'; +import { getProspectById, insertProspect, truncateDb } from './dynamodb'; +import { createProspect } from './test/helpers'; + +// use chai's expect (not jest's) +const expect = chai.expect; + +describe('dynamodb.common', () => { + beforeAll(async () => { + dbClient.destroy(); + }); + afterEach(async () => { + await truncateDb(dbClient); + }); + afterAll(async () => { + dbClient.destroy(); + }); + + describe('insertProspect', () => { + it('should insert a prospect', async () => { + const prospect = createProspect('EN_US', ProspectType.TIMESPENT); + + await insertProspect(dbClient, prospect); + + const res = await getProspectById(dbClient, prospect.id); + + expect(res).not.to.be.undefined; + }); + }); +}); diff --git a/packages/prospectapi-common/dynamodb.spec.ts b/packages/prospectapi-common/dynamodb.spec.ts new file mode 100644 index 00000000..bd897584 --- /dev/null +++ b/packages/prospectapi-common/dynamodb.spec.ts @@ -0,0 +1,47 @@ +import { expect } from 'chai'; +import { Prospect, Topics, ProspectType } from './types'; +import { toUnixTimestamp } from './lib'; +import { generateInsertParams } from './dynamodb'; + +describe('dynamodb.common', () => { + let prospect: Prospect; + + beforeEach(() => { + prospect = { + id: 'LeGuId', + prospectId: 'LeProspectGuid', + scheduledSurfaceGuid: 'NEW_TAB_EN_US', + rank: 10, + topic: Topics.GAMING, + prospectType: ProspectType.TIMESPENT, + url: 'https://getpocket.com', + saveCount: 2100, + }; + }); + + describe('generateInsertParams', () => { + it('should correctly map prospect into a dynamodb item', () => { + const params = generateInsertParams(prospect); + + expect(params.Item?.id).to.equal(prospect.id); + expect(params.Item?.scheduledSurfaceGuid).to.equal( + prospect.scheduledSurfaceGuid, + ); + expect(params.Item?.rank).to.equal(prospect.rank); + expect(params.Item?.url).to.equal(prospect.url); + expect(params.Item?.topic).to.equal(prospect.topic); + expect(params.Item?.prospectType).to.equal(prospect.prospectType); + expect(params.Item?.createdAt).not.be.null; + }); + + it('should generate a `createdAt` date of the current time', () => { + // we're just making sure the `createdAt` value is greater than one minute ago + const params = generateInsertParams(prospect); + + const now = toUnixTimestamp(); + const oneMinuteAgo = toUnixTimestamp(new Date(now - 6000)); + + expect(params.Item?.createdAt).to.be.greaterThan(oneMinuteAgo); + }); + }); +}); diff --git a/packages/prospectapi-common/dynamodb.ts b/packages/prospectapi-common/dynamodb.ts new file mode 100644 index 00000000..aa33c9c1 --- /dev/null +++ b/packages/prospectapi-common/dynamodb.ts @@ -0,0 +1,133 @@ +import { + DeleteCommand, + DynamoDBDocumentClient, + GetCommand, + GetCommandInput, + PutCommand, + PutCommandInput, + ScanCommand, + ScanCommandOutput, +} from '@aws-sdk/lib-dynamodb'; + +import config from './config'; +import { DynamoItem, Prospect } from './types'; +import { toUnixTimestamp } from './lib'; + +/** + * test helper method for integration tests. essentially a way to retrieve all + * rows in the database - for counting or truncating. + * + * @returns ScanCommandOutput - object containing, among other things, an + * array of Items + */ +export const scanAllRows = async ( + dbClient: DynamoDBDocumentClient, +): Promise => { + return await dbClient.send( + new ScanCommand({ + TableName: config.aws.dynamoDb.table, + AttributesToGet: ['id'], + }), + ); +}; + +/** + * converts a Prospect into an expected dynamo record with timestamp + * + * @param prospect a Prospect object + * @returns DynamoDB.PutItemInput + */ +export const generateInsertParams = (prospect: Prospect): PutCommandInput => { + // in a "live" scenario, the prospect will never have a `createdAt` at + // the time of insertion. + // however, for tests we need to set this explicitly. + const createdAt = prospect.createdAt || toUnixTimestamp(); + + // again, in a live scenario, this will always be false. for testing, we + // need to insert pre-curated prospects + const curated = prospect.curated || false; + + return { + TableName: config.aws.dynamoDb.table, + Item: { + id: prospect.id, // custom GUID + // GUID supplied by ML - not unique on purpose! + prospectId: prospect.prospectId, + scheduledSurfaceGuid: prospect.scheduledSurfaceGuid, + url: prospect.url, + topic: prospect.topic, + prospectType: prospect.prospectType, + saveCount: prospect.saveCount, + rank: prospect.rank, + createdAt, + curated, + domain: prospect.domain, + excerpt: prospect.excerpt, + imageUrl: prospect.imageUrl, + language: prospect.language, + publisher: prospect.publisher, + title: prospect.title, + isSyndicated: prospect.isSyndicated, + isCollection: prospect.isCollection, + authors: prospect.authors, + }, + }; +}; + +/** + * test helper method for integration tests + * + * note - this will only delete a max of 1MB of data, but we should never + * hit that in our integration tests + */ +export const truncateDb = async ( + dbClient: DynamoDBDocumentClient, +): Promise => { + const rows = await scanAllRows(dbClient); + + rows.Items?.forEach(async function (element, _) { + await dbClient.send( + new DeleteCommand({ + TableName: config.aws.dynamoDb.table, + Key: element, + }), + ); + }); +}; + +/** + * inserts a prospect into dynamo + * + * @param prospect a Prospect object + */ +export const insertProspect = async ( + dbClient: DynamoDBDocumentClient, + prospect: Prospect, +): Promise => { + // convert the prospect to dynamo insert format + const params = generateInsertParams(prospect); + + await dbClient.send(new PutCommand(params)); +}; + +/** + * test helper method to verify deletes are working as expected + * + * @param id string id of a prospect row in db + * @returns raw row data from dynamo, undefined if the id wasn't found + */ +export const getProspectById = async ( + dbClient: DynamoDBDocumentClient, + id: string, +): Promise => { + const input: GetCommandInput = { + TableName: config.aws.dynamoDb.table, + Key: { + id: id, + }, + }; + + const res = await dbClient.send(new GetCommand(input)); + + return res.Item; +}; diff --git a/packages/prospectapi-common/index.d.ts b/packages/prospectapi-common/index.d.ts new file mode 100644 index 00000000..6640acd4 --- /dev/null +++ b/packages/prospectapi-common/index.d.ts @@ -0,0 +1,21 @@ +export { + CorpusLanguage, + DynamoItem, + GetProspectsFilters, + Prospect, + ProspectType, + ScheduledSurfaces, + ScheduledSurface, + Topics, + UrlMetadata, +} from './types'; +export { toUnixTimestamp, deriveUrlMetadata } from './lib'; +export { + scanAllRows, + generateInsertParams, + truncateDb, + insertProspect, + getProspectById, +} from './dynamodb'; +export { dbClient } from './dynamodb-client'; +export { createProspect } from './test/helpers'; diff --git a/packages/prospectapi-common/index.js b/packages/prospectapi-common/index.js new file mode 100644 index 00000000..601fa5a2 --- /dev/null +++ b/packages/prospectapi-common/index.js @@ -0,0 +1,22 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.createProspect = exports.dbClient = exports.getProspectById = exports.insertProspect = exports.truncateDb = exports.generateInsertParams = exports.scanAllRows = exports.deriveUrlMetadata = exports.toUnixTimestamp = exports.Topics = exports.ScheduledSurfaces = exports.ProspectType = exports.CorpusLanguage = void 0; +var types_1 = require("./types"); +Object.defineProperty(exports, "CorpusLanguage", { enumerable: true, get: function () { return types_1.CorpusLanguage; } }); +Object.defineProperty(exports, "ProspectType", { enumerable: true, get: function () { return types_1.ProspectType; } }); +Object.defineProperty(exports, "ScheduledSurfaces", { enumerable: true, get: function () { return types_1.ScheduledSurfaces; } }); +Object.defineProperty(exports, "Topics", { enumerable: true, get: function () { return types_1.Topics; } }); +var lib_1 = require("./lib"); +Object.defineProperty(exports, "toUnixTimestamp", { enumerable: true, get: function () { return lib_1.toUnixTimestamp; } }); +Object.defineProperty(exports, "deriveUrlMetadata", { enumerable: true, get: function () { return lib_1.deriveUrlMetadata; } }); +var dynamodb_1 = require("./dynamodb"); +Object.defineProperty(exports, "scanAllRows", { enumerable: true, get: function () { return dynamodb_1.scanAllRows; } }); +Object.defineProperty(exports, "generateInsertParams", { enumerable: true, get: function () { return dynamodb_1.generateInsertParams; } }); +Object.defineProperty(exports, "truncateDb", { enumerable: true, get: function () { return dynamodb_1.truncateDb; } }); +Object.defineProperty(exports, "insertProspect", { enumerable: true, get: function () { return dynamodb_1.insertProspect; } }); +Object.defineProperty(exports, "getProspectById", { enumerable: true, get: function () { return dynamodb_1.getProspectById; } }); +var dynamodb_client_1 = require("./dynamodb-client"); +Object.defineProperty(exports, "dbClient", { enumerable: true, get: function () { return dynamodb_client_1.dbClient; } }); +var helpers_1 = require("./test/helpers"); +Object.defineProperty(exports, "createProspect", { enumerable: true, get: function () { return helpers_1.createProspect; } }); +//# sourceMappingURL=index.js.map \ No newline at end of file diff --git a/packages/prospectapi-common/index.js.map b/packages/prospectapi-common/index.js.map new file mode 100644 index 00000000..213cc559 --- /dev/null +++ b/packages/prospectapi-common/index.js.map @@ -0,0 +1 @@ +{"version":3,"file":"index.js","sourceRoot":"/","sources":["../prospectapi-common/index.ts"],"names":[],"mappings":";;;AAIA,iCAUiB;AATf,uGAAA,cAAc,OAAA;AAId,qGAAA,YAAY,OAAA;AACZ,0GAAA,iBAAiB,OAAA;AAEjB,+FAAA,MAAM,OAAA;AAGR,6BAA2D;AAAlD,sGAAA,eAAe,OAAA;AAAE,wGAAA,iBAAiB,OAAA;AAC3C,uCAMoB;AALlB,uGAAA,WAAW,OAAA;AACX,gHAAA,oBAAoB,OAAA;AACpB,sGAAA,UAAU,OAAA;AACV,0GAAA,cAAc,OAAA;AACd,2GAAA,eAAe,OAAA;AAEjB,qDAA6C;AAApC,2GAAA,QAAQ,OAAA;AACjB,0CAAgD;AAAvC,yGAAA,cAAc,OAAA","sourcesContent":["// i don't love combining all of this - types, db functions, lib functions - in\n// a single entrypoint, BUT typescript (as of 2021-12-02) doesn't support\n// multiple entry points in package.json - so we're kind of stuck with this.\n\nexport {\n CorpusLanguage,\n DynamoItem,\n GetProspectsFilters,\n Prospect,\n ProspectType,\n ScheduledSurfaces,\n ScheduledSurface,\n Topics,\n UrlMetadata,\n} from './types';\nexport { toUnixTimestamp, deriveUrlMetadata } from './lib';\nexport {\n scanAllRows,\n generateInsertParams,\n truncateDb,\n insertProspect,\n getProspectById,\n} from './dynamodb';\nexport { dbClient } from './dynamodb-client';\nexport { createProspect } from './test/helpers';\n"]} \ No newline at end of file diff --git a/packages/prospectapi-common/index.ts b/packages/prospectapi-common/index.ts new file mode 100644 index 00000000..0ccf6a5b --- /dev/null +++ b/packages/prospectapi-common/index.ts @@ -0,0 +1,25 @@ +// i don't love combining all of this - types, db functions, lib functions - in +// a single entrypoint, BUT typescript (as of 2021-12-02) doesn't support +// multiple entry points in package.json - so we're kind of stuck with this. + +export { + CorpusLanguage, + DynamoItem, + GetProspectsFilters, + Prospect, + ProspectType, + ScheduledSurfaces, + ScheduledSurface, + Topics, + UrlMetadata, +} from './types'; +export { toUnixTimestamp, deriveUrlMetadata } from './lib'; +export { + scanAllRows, + generateInsertParams, + truncateDb, + insertProspect, + getProspectById, +} from './dynamodb'; +export { dbClient } from './dynamodb-client'; +export { createProspect } from './test/helpers'; diff --git a/packages/prospectapi-common/jest.config.js b/packages/prospectapi-common/jest.config.js new file mode 100644 index 00000000..ae85a274 --- /dev/null +++ b/packages/prospectapi-common/jest.config.js @@ -0,0 +1,7 @@ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', + testMatch: ['**/?(*.)+(spec|integration).ts'], + testPathIgnorePatterns: ['/dist/'], + setupFiles: ['./jest.setup.js'], +}; diff --git a/packages/prospectapi-common/jest.setup.js b/packages/prospectapi-common/jest.setup.js new file mode 100644 index 00000000..5c24690b --- /dev/null +++ b/packages/prospectapi-common/jest.setup.js @@ -0,0 +1,3 @@ +// AWS_ENDPOINT is set in .docker/local.env +// this file is to provide a value when running tests outside of the container +process.env.AWS_ENDPOINT = process.env.AWS_ENDPOINT || 'http://localhost:4566'; diff --git a/packages/prospectapi-common/lib.spec.ts b/packages/prospectapi-common/lib.spec.ts new file mode 100644 index 00000000..40b5243c --- /dev/null +++ b/packages/prospectapi-common/lib.spec.ts @@ -0,0 +1,286 @@ +import { expect } from 'chai'; +import { + deriveAuthors, + deriveDomainName, + deriveExcerpt, + deriveImageUrl, + derivePublisher, + deriveTitle, + toUnixTimestamp, +} from './lib'; +import { ClientApiItem } from './types'; + +describe('lib', () => { + describe('toUnixTimestamp', () => { + it('should convert to a unix timestamp', () => { + const now = new Date(); + + const timestamp = toUnixTimestamp(now); + + const timestampBackToDate = new Date(timestamp * 1000); + + expect(timestampBackToDate.getUTCFullYear()).to.equal( + now.getUTCFullYear(), + ); + expect(timestampBackToDate.getUTCMonth()).to.equal(now.getUTCMonth()); + expect(timestampBackToDate.getUTCDate()).to.equal(now.getUTCDate()); + expect(timestampBackToDate.getUTCHours()).to.equal(now.getUTCHours()); + expect(timestampBackToDate.getUTCMinutes()).to.equal(now.getUTCMinutes()); + expect(timestampBackToDate.getUTCSeconds()).to.equal(now.getUTCSeconds()); + }); + + it('should give a unix timestamp for now if no date provided', () => { + const now = new Date(); + + const timestamp = toUnixTimestamp(); + + const timestampBackToDate = new Date(timestamp * 1000); + + expect(timestampBackToDate.getUTCFullYear()).to.equal( + now.getUTCFullYear(), + ); + expect(timestampBackToDate.getUTCMonth()).to.equal(now.getUTCMonth()); + expect(timestampBackToDate.getUTCDate()).to.equal(now.getUTCDate()); + expect(timestampBackToDate.getUTCHours()).to.equal(now.getUTCHours()); + expect(timestampBackToDate.getUTCMinutes()).to.equal(now.getUTCMinutes()); + expect(timestampBackToDate.getUTCSeconds()).to.equal(now.getUTCSeconds()); + }); + }); + + describe('derivePublisher', () => { + it('should return the syndicated publisher if exists', () => { + const item: ClientApiItem = { + resolvedUrl: 'https://getpocket.com/idk', + syndicatedArticle: { + authorNames: [], + publisher: { + name: 'The Daily Bugle', + url: 'https://thedailybugle.com', + }, + title: 'Silly As Needed', + }, + }; + + expect(derivePublisher(item)).to.equal('The Daily Bugle'); + }); + + it('should return the domainMetadata publisher if exists', () => { + const item: ClientApiItem = { + domainMetadata: { + name: 'The Daily Planet', + }, + resolvedUrl: 'https://getpocket.com/idk', + }; + + expect(derivePublisher(item)).to.equal('The Daily Planet'); + }); + + it('should return an empty string if no publisher exists', () => { + const item: ClientApiItem = { + resolvedUrl: 'https://getpocket.com/idk', + }; + + expect(derivePublisher(item)).to.equal(''); + }); + }); + + describe('deriveDomainName', () => { + it('should return the syndicated article domain if exists', () => { + expect( + deriveDomainName( + 'https://getpocket.com/idk', + 'https://thedailybugle.com/happy/reading/all', + ), + ).to.equal('thedailybugle.com'); + }); + + it('should return the prospect domain if no syndicated url exists', () => { + expect( + deriveDomainName('https://thedailyplanet.com/some/article/here'), + ).to.equal('thedailyplanet.com'); + }); + }); + + describe('deriveAuthors', () => { + describe('authors directly on the item (non-syndicated)', () => { + it('should return a CSV for multiple authors', () => { + const item: ClientApiItem = { + resolvedUrl: 'https://getpocket.com/silly-as-needed', + authors: [ + { name: 'Samantha Irby' }, + { name: 'Questlove' }, + { name: 'Noam Chomsky' }, + ], + }; + + expect(deriveAuthors(item)).to.equal( + 'Samantha Irby,Questlove,Noam Chomsky', + ); + }); + + it('should remove preceding/trailing spaces in authors', () => { + const item: ClientApiItem = { + resolvedUrl: 'https://getpocket.com/silly-as-needed', + authors: [{ name: ' Samantha Irby ' }, { name: 'Questlove ' }], + }; + + expect(deriveAuthors(item)).to.equal('Samantha Irby,Questlove'); + }); + + it('should remove empty authors', () => { + const item: ClientApiItem = { + resolvedUrl: 'https://getpocket.com/silly-as-needed', + authors: [{ name: '' }, { name: 'Questlove' }], + }; + + expect(deriveAuthors(item)).to.equal('Questlove'); + }); + + it('should return an empty string for no authors', () => { + const item: ClientApiItem = { + resolvedUrl: 'https://getpocket.com/silly-as-needed', + }; + + expect(deriveAuthors(item)).to.equal(''); + }); + }); + + describe('syndicated authors', () => { + it('should return a CSV string for multiple authors', () => { + const item: ClientApiItem = { + resolvedUrl: 'https://getpocket.com/silly-as-needed', + syndicatedArticle: { + authorNames: ['Octavia Butler', 'V.E. Schwab'], + title: 'Silly As Needed', + }, + }; + + expect(deriveAuthors(item)).to.equal('Octavia Butler,V.E. Schwab'); + }); + + it('should return an empty string for no authors', () => { + const item: ClientApiItem = { + resolvedUrl: 'https://getpocket.com/silly-as-needed', + syndicatedArticle: { + authorNames: [], + title: 'Silly As Needed', + }, + }; + + expect(deriveAuthors(item)).to.equal(''); + }); + }); + }); + + describe('deriveExcerpt', () => { + it('should use the syndicated excerpt if it exists', () => { + const item: ClientApiItem = { + excerpt: 'All right meow, hand over your license and registration...', + resolvedUrl: 'https://getpocket.com/silly-as-needed', + syndicatedArticle: { + authorNames: [], + excerpt: 'Your registraton? Hurry up meow.', + title: 'Silly As Needed', + }, + }; + + expect(deriveExcerpt(item)).to.equal('Your registraton? Hurry up meow.'); + }); + + it('should use the item excerpt if no syndicated excerpt exists', () => { + const item: ClientApiItem = { + excerpt: 'All right meow, hand over your license and registration...', + resolvedUrl: 'https://getpocket.com/silly-as-needed', + syndicatedArticle: { + authorNames: [], + title: 'Silly As Needed', + }, + }; + + expect(deriveExcerpt(item)).to.equal( + 'All right meow, hand over your license and registration...', + ); + }); + + it('should return undefined if no excerpt exists', () => { + const item: ClientApiItem = { + resolvedUrl: 'https://getpocket.com/silly-as-needed', + syndicatedArticle: { + authorNames: [], + title: 'Silly As Needed', + }, + }; + + expect(deriveExcerpt(item)).to.equal(undefined); + }); + }); + + describe('deriveTitle', () => { + it('should use the syndicated title if it exists', () => { + const item: ClientApiItem = { + title: 'Ragnarok', + resolvedUrl: 'https://getpocket.com/silly-as-needed', + syndicatedArticle: { + authorNames: [], + title: 'Silly As Needed', + }, + }; + + expect(deriveTitle(item)).to.equal('Silly As Needed'); + }); + + it('should use the item title if no syndicated title exists', () => { + const item: ClientApiItem = { + title: 'Ragnarok', + resolvedUrl: 'https://getpocket.com/silly-as-needed', + }; + + expect(deriveTitle(item)).to.equal('Ragnarok'); + }); + + it('should return undefined if no title exists', () => { + const item: ClientApiItem = { + resolvedUrl: 'https://getpocket.com/silly-as-needed', + }; + + expect(deriveTitle(item)).to.equal(undefined); + }); + }); + + describe('deriveImageUrl', () => { + it('should use the syndicated image if it exists', () => { + const item: ClientApiItem = { + resolvedUrl: 'https://getpocket.com/silly-as-needed', + topImageUrl: 'https://www.placecage.com/g/300/200', + syndicatedArticle: { + authorNames: [], + title: 'Silly As Needed', + mainImage: 'https://www.placecage.com/g/300/300', + }, + }; + + expect(deriveImageUrl(item)).to.equal( + 'https://www.placecage.com/g/300/300', + ); + }); + + it('should use the item image if no syndicated title exists', () => { + const item: ClientApiItem = { + topImageUrl: 'https://www.placecage.com/g/300/200', + resolvedUrl: 'https://getpocket.com/silly-as-needed', + }; + + expect(deriveImageUrl(item)).to.equal( + 'https://www.placecage.com/g/300/200', + ); + }); + + it('should return undefined if no image exists', () => { + const item: ClientApiItem = { + resolvedUrl: 'https://getpocket.com/silly-as-needed', + }; + + expect(deriveImageUrl(item)).to.equal(undefined); + }); + }); +}); diff --git a/packages/prospectapi-common/lib.ts b/packages/prospectapi-common/lib.ts new file mode 100644 index 00000000..c8067067 --- /dev/null +++ b/packages/prospectapi-common/lib.ts @@ -0,0 +1,161 @@ +import { parse } from 'tldts'; +import * as Sentry from '@sentry/node'; +import { getUrlMetadata } from './client-api-proxy'; +import { UrlMetadata, ClientApiItem } from './types'; + +/** + * helper to convert a JS Date to a unix timestamp. note that this will lose + * milisecond information (as unix timestamps are seconds based). + * + * @param date a Date object + * @returns a unix timestamp number + */ +export const toUnixTimestamp = (date?: Date): number => { + date = date || new Date(); + + // JS `getTime()` returns miliseconds, while unix timestamp expects seconds + // this is why we divide by 1000 + return Math.floor(date.getTime() / 1000); +}; + +/** + * returns the domain of the original publisher if it's a syndicated article, + * otherwise returns the domain of the article's original url. + * + * @param resolvedUrl string URL of the article + * @param syndicatedArticleUrl string URL of the syndicated article + * @returns string domain name or undefined + */ +export const deriveDomainName = ( + resolvedUrl: string, + syndicatedArticleUrl?: string, +): string | undefined => { + const domain = syndicatedArticleUrl + ? parse(syndicatedArticleUrl).domain + : parse(resolvedUrl).domain; + + return domain || undefined; +}; + +/** + * takes a parser item and, if it's a syndicated article, returns the + * original publisher (instead of "Pocket"). otherwise, returns the publisher + * from the domain. returns an empty string if no publisher could be derived. + * + * @param item ClientApiItem the raw item from client API + * @returns string + */ +export const derivePublisher = (item: ClientApiItem): string => { + let publisher: string; + + if (item.syndicatedArticle?.publisher?.name) { + publisher = item.syndicatedArticle.publisher.name; + } else if (item.domainMetadata?.name) { + publisher = item.domainMetadata.name; + } else { + // log to sentry so we can see recommendations without publishers + Sentry.captureException( + new Error( + `No publisher could be derived for resolvedUrl: ${item.resolvedUrl}`, + ), + ); + + publisher = ''; + } + + return publisher; +}; + +/** + * creates a CSV of authors from the array returned by client API/parser + * + * @param item ClientApiItem - raw item from client API + * @returns string - a CSV string of all author names, empty if no authors were returned from the Parser + */ +export const deriveAuthors = (item: ClientApiItem): string => { + let authors = ''; + + if (item.syndicatedArticle?.authorNames.length) { + // if it's a syndicated article and has author names data + // (syndicated authors data is just an array of name strings) + authors = item.syndicatedArticle?.authorNames.join(','); + } else if (item.authors) { + // if not a syndicated article and has authors data + // (item authors data is an array of objects, each with a `name` property) + item.authors.forEach((author) => { + // make sure we have actual data + // not checking for alpha only here, as author names could contain really any character + if (author.name.trim().length) { + authors = authors.concat(author.name.trim(), ','); + } + }); + + // remove the trailing comma + authors = authors.slice(0, -1); + } + + return authors; +}; + +/** + * returns the syndicated excerpt if it exists, otherwise the item's excerpt + * (result could be undefined - ty parser!) + * + * @param item ClientApiItem - raw item from client API + * @returns string + */ +export const deriveExcerpt = (item: ClientApiItem): string | undefined => { + return item.syndicatedArticle?.excerpt || item.excerpt; +}; + +/** + * returns the syndicated title if it exists, otherwise the item's title + * (result could be undefined - ty parser!) + * + * @param item ClientApiItem - raw item from client API + * @returns string | undefined + */ +export const deriveTitle = (item: ClientApiItem): string | undefined => { + return item.syndicatedArticle?.title || item.title; +}; + +/** + * returns the syndicated image url if it exists, otherwise the item's image url + * (result could be undefined - ty parser!) + * + * @param item ClientApiItem - raw item from client API + * @returns string + */ +export const deriveImageUrl = (item: ClientApiItem): string | undefined => { + return item.syndicatedArticle?.mainImage || item.topImageUrl; +}; + +export const deriveUrlMetadata = async (url: string): Promise => { + // get the meta data from the parser + const item = await getUrlMetadata(url); + + // return fully populated meta data if metadata is returned from the parser + if (item) { + return { + url: item.resolvedUrl, + domain: deriveDomainName( + item.resolvedUrl, + item?.syndicatedArticle?.publisher?.url, + ), + imageUrl: deriveImageUrl(item), + publisher: derivePublisher(item), + title: deriveTitle(item), + excerpt: deriveExcerpt(item), + language: item.language, + isSyndicated: !!item.syndicatedArticle?.publisher, + isCollection: !!item.collection?.slug, + authors: deriveAuthors(item), + }; + } else { + // fallback to return the original url and domain + return { + url: url, + domain: deriveDomainName(url), + }; + } +}; diff --git a/packages/prospectapi-common/package.json b/packages/prospectapi-common/package.json new file mode 100644 index 00000000..5e87839b --- /dev/null +++ b/packages/prospectapi-common/package.json @@ -0,0 +1,35 @@ +{ + "name": "prospectapi-common", + "version": "1.0.0", + "description": "common code shared between api and lambda", + "main": "./dist/index.js", + "scripts": { + "build": "rm -rf dist && tsc", + "test": "jest \"\\.spec\\.ts\"", + "test-integrations": "jest \"\\.integration\\.ts\" --runInBand", + "lint": "eslint --fix-dry-run \"**/*.ts\" \"test/**/*.ts\"", + "format": "eslint --fix \"**/*.ts\" \"test/**/*.ts\"" + }, + "author": "pocket", + "license": "ISC", + "private": true, + "dependencies": { + "@apollo/client": "3.7.16", + "@aws-sdk/client-dynamodb": "3.391.0", + "@aws-sdk/lib-dynamodb": "3.391.0", + "@sentry/node": "6.19.7", + "@faker-js/faker": "6.0.0", + "graphql-tag": "2.12.6", + "cross-fetch": "3.1.5", + "tldts": "5.7.87" + }, + "devDependencies": { + "eslint-config-custom": "workspace:*", + "tsconfig": "workspace:*", + "@types/chai": "4.3.4", + "@types/jest": "28.1.7", + "chai": "4.3.7", + "jest": "28.1.3", + "ts-jest": "28.0.8" + } +} diff --git a/packages/prospectapi-common/test/helpers.ts b/packages/prospectapi-common/test/helpers.ts new file mode 100644 index 00000000..b1e1d107 --- /dev/null +++ b/packages/prospectapi-common/test/helpers.ts @@ -0,0 +1,59 @@ +import { faker } from '@faker-js/faker'; + +import { CorpusLanguage, Prospect, ProspectType, Topics } from '../types'; + +// turn the enum into an array so we can grab a random one easily +const topicsArray = Object.keys(Topics).map((key) => Topics[key]); + +/** + * creates a Prospect object with the specified scheduledSurface, prospectType, + * and curated status. the rest of the data will be faker faked. + * + * this may warrant future refactoring if we want control over more than just + * the three data points / parameters + * + * @param scheduledSurfaceGuid string new tab GUID + * @param prospectType ProspectType enum value + * @param curated boolean + * @returns Prospect + */ +export const createProspect = ( + scheduledSurfaceGuid: string, + prospectType: ProspectType, + curated = false, +): Prospect => { + // randomize number of authors + const authorCount = faker.datatype.number({ min: 1, max: 3 }); + const authors: string[] = []; + + for (let i = 0; i < authorCount; i++) { + authors.push(faker.name.findName()); + } + + return { + id: faker.datatype.uuid(), + prospectId: faker.datatype.uuid(), + scheduledSurfaceGuid, + topic: faker.random.arrayElement(topicsArray), + prospectType, + url: faker.internet.url(), + rank: faker.datatype.number(), + saveCount: faker.datatype.number(), + curated, + createdAt: Math.floor(faker.date.recent().valueOf() / 1000), + domain: faker.internet.domainName(), + excerpt: faker.lorem.paragraph(), + imageUrl: faker.internet.url(), + language: faker.random.arrayElement(Object.values(CorpusLanguage)), + publisher: faker.random.arrayElement([ + 'The New York Times', + 'The Atlantic', + 'The Guardian', + 'The Register', + ]), + title: faker.lorem.words(), + isSyndicated: faker.datatype.boolean(), + isCollection: faker.datatype.boolean(), + authors: authors.join(','), + }; +}; diff --git a/packages/prospectapi-common/tsconfig.json b/packages/prospectapi-common/tsconfig.json new file mode 100644 index 00000000..e4b10e5b --- /dev/null +++ b/packages/prospectapi-common/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "tsconfig/graphql.json", + "compilerOptions": { + "outDir": "./dist", + }, + "exclude": ["node_modules", "**/*.spec.ts", "**/*.integration.ts"], + "include": ["*.ts"] +} diff --git a/packages/prospectapi-common/types.ts b/packages/prospectapi-common/types.ts new file mode 100644 index 00000000..1713e933 --- /dev/null +++ b/packages/prospectapi-common/types.ts @@ -0,0 +1,282 @@ +// much of the data in this file comes from our shared data repository, which +// currently is a confluence doc: +// https://getpocket.atlassian.net/wiki/spaces/PE/pages/2584150049/Pocket+Shared+Data +import { NativeAttributeValue } from '@aws-sdk/util-dynamodb'; + +// this is the structure of an `Item` as returned by dynamo +// just a convenience return type +export type DynamoItem = + | { + [key: string]: NativeAttributeValue; +} + | undefined; + +// we may want to move these enums/types to a more shareable location +// will refactor if/when needed +export enum Topics { + BUSINESS = 'BUSINESS', + CAREER = 'CAREER', + CORONAVIRUS = 'CORONAVIRUS', + EDUCATION = 'EDUCATION', + ENTERTAINMENT = 'ENTERTAINMENT', + FOOD = 'FOOD', + GAMING = 'GAMING', + HEALTH_FITNESS = 'HEALTH_FITNESS', + PARENTING = 'PARENTING', + PERSONAL_FINANCE = 'PERSONAL_FINANCE', + POLITICS = 'POLITICS', + SCIENCE = 'SCIENCE', + SELF_IMPROVEMENT = 'SELF_IMPROVEMENT', + SPORTS = 'SPORTS', + TECHNOLOGY = 'TECHNOLOGY', + TRAVEL = 'TRAVEL', +} + +// these values will need to match those listed in the source of truth doc: +// https://mozilla-hub.atlassian.net/wiki/spaces/PE/pages/390642851/Pocket+Shared+Data#Prospect-Types +export enum ProspectType { + TIMESPENT = 'TIMESPENT', + COUNTS = 'COUNTS', + SYNDICATED_NEW = 'SYNDICATED_NEW', + SYNDICATED_RERUN = 'SYNDICATED_RERUN', + DOMAIN_ALLOWLIST = 'DOMAIN_ALLOWLIST', + TOP_SAVED = 'TOP_SAVED', + RECOMMENDED = 'RECOMMENDED', + COUNTS_MODELED = 'COUNTS_MODELED', + TIMESPENT_MODELED = 'TIMESPENT_MODELED', + TITLE_URL_MODELED = 'TITLE_URL_MODELED', + RSS_LOGISTIC = 'RSS_LOGISTIC', + RSS_LOGISTIC_RECENT = 'RSS_LOGISTIC_RECENT', + DISMISSED = 'DISMISSED', + CONSTRAINT_SCHEDULE = 'CONSTRAINT_SCHEDULE', +} + +// languages we support in the corpus +export enum CorpusLanguage { + EN = 'EN', + DE = 'DE', + ES = 'ES', + FR = 'FR', + IT = 'IT', +} + +// this is the type used in most of the code and in dynamo +export type Prospect = { + // a GUID we generate prior to inserting into dynamo + id: string; + // the prospect ID supplied by ML + prospectId: string; + // this will match the name in ScheduledSurfaces (below) + // should this map to that type? would make lookups/type validation a pain... + // however, this value *is* validated against the array below when coming + // from sqs/before being inserted into dynamo, so checking does occur + scheduledSurfaceGuid: string; + topic?: Topics; + prospectType: ProspectType; + url: string; + saveCount: number; + rank: number; + curated?: boolean; + // unix timestamp + createdAt?: number; + // below properties will be populated via client api/parser + domain?: string; + excerpt?: string; + imageUrl?: string; + language?: string; + publisher?: string; + title?: string; + isSyndicated?: boolean; + isCollection?: boolean; + // authors will be a comma separated string + authors?: string; + approvedCorpusItem?: { url: string }; + rejectedCorpusItem?: { url: string }; +}; + +// a scheduled surface has a name as well as an array of associated ProspectTypes +export type ScheduledSurface = { + name: string; + guid: string; + ianaTimezone: string; + prospectTypes: ProspectType[]; +}; + +// all the filters on the `getProspects` query +export type GetProspectsFilters = { + scheduledSurfaceGuid: string; + prospectType?: ProspectType; + includePublisher?: string; + excludePublisher?: string; +}; + +// defines all scheduled surfaces and their valid prospect types +// this will need to be kept in-sync with the source of truth confluence: +// https://getpocket.atlassian.net/wiki/spaces/PE/pages/2564587582/Prospecting+Candidate+Sets +// (this data is used in both ML & backend processes - is there a better +// place to store/reference it? probably not at the moment...) +export const ScheduledSurfaces: ScheduledSurface[] = [ + { + name: 'New Tab (en-US)', + guid: 'NEW_TAB_EN_US', + ianaTimezone: 'America/New_York', + prospectTypes: [ + ProspectType.COUNTS, + ProspectType.TIMESPENT, + ProspectType.RECOMMENDED, + ProspectType.TOP_SAVED, + ProspectType.DOMAIN_ALLOWLIST, + ProspectType.DISMISSED, + ProspectType.SYNDICATED_RERUN, + ProspectType.SYNDICATED_NEW, + ProspectType.COUNTS_MODELED, + ProspectType.TIMESPENT_MODELED, + ProspectType.TITLE_URL_MODELED, + ProspectType.RSS_LOGISTIC, + ProspectType.RSS_LOGISTIC_RECENT, + ProspectType.CONSTRAINT_SCHEDULE, + ], + }, + { + name: 'New Tab (de-DE)', + guid: 'NEW_TAB_DE_DE', + ianaTimezone: 'Europe/Berlin', + prospectTypes: [ + ProspectType.COUNTS, + ProspectType.TIMESPENT, + ProspectType.DOMAIN_ALLOWLIST, + ProspectType.DISMISSED, + ProspectType.TITLE_URL_MODELED, + ], + }, + { + name: 'New Tab (en-GB)', + guid: 'NEW_TAB_EN_GB', + ianaTimezone: 'Europe/London', + prospectTypes: [ + ProspectType.COUNTS, + ProspectType.TIMESPENT, + ProspectType.RECOMMENDED, + ProspectType.DISMISSED, + ], + }, + { + name: 'New Tab (fr-FR)', + guid: 'NEW_TAB_FR_FR', + ianaTimezone: 'Europe/Paris', + prospectTypes: [ProspectType.DOMAIN_ALLOWLIST], + }, + { + name: 'New Tab (it-IT)', + guid: 'NEW_TAB_IT_IT', + ianaTimezone: 'Europe/Rome', + prospectTypes: [ProspectType.DOMAIN_ALLOWLIST], + }, + { + name: 'New Tab (es-ES)', + guid: 'NEW_TAB_ES_ES', + ianaTimezone: 'Europe/Madrid', + prospectTypes: [ProspectType.DOMAIN_ALLOWLIST], + }, + { + name: 'New Tab (en-INTL)', + guid: 'NEW_TAB_EN_INTL', + ianaTimezone: 'Asia/Kolkata', + prospectTypes: [ + ProspectType.COUNTS, + ProspectType.TIMESPENT, + ProspectType.RECOMMENDED, + ProspectType.TITLE_URL_MODELED, + ProspectType.DISMISSED, + ], + }, + { + name: 'Pocket Hits (en-US)', + guid: 'POCKET_HITS_EN_US', + ianaTimezone: 'America/New_York', + prospectTypes: [ + ProspectType.COUNTS, + ProspectType.TOP_SAVED, + ProspectType.TIMESPENT, + ProspectType.DISMISSED, + ProspectType.COUNTS_MODELED, + ProspectType.TITLE_URL_MODELED, + ProspectType.RSS_LOGISTIC, + ProspectType.SYNDICATED_NEW, + ProspectType.SYNDICATED_RERUN, + ], + }, + { + name: 'Pocket Hits (de-DE)', + guid: 'POCKET_HITS_DE_DE', + ianaTimezone: 'Europe/Berlin', + prospectTypes: [ + ProspectType.COUNTS, + ProspectType.TIMESPENT, + ProspectType.TOP_SAVED, + ProspectType.DOMAIN_ALLOWLIST, + ProspectType.TITLE_URL_MODELED, + ], + }, + { + name: 'Sandbox', + guid: 'SANDBOX', + ianaTimezone: 'America/New_York', + prospectTypes: [], + }, +]; + +export type ClientApiResponse = { + data: { + getItemByUrl: ClientApiItem; + }; +}; + +export type ClientApiDomainMeta = { + name: string; +}; + +export type ClientApiSyndicatedArticle = { + authorNames: string[]; + excerpt?: string; + mainImage?: string; + publisher?: { + name?: string; + url?: string; + }; + title: string; +}; + +export type ClientApiCollection = { + slug: string; +}; + +export type ClientApiAuthor = { + name: string; +}; + +export type ClientApiItem = { + domainMetadata?: ClientApiDomainMeta; + excerpt?: string; + language?: string; + resolvedUrl: string; + syndicatedArticle?: ClientApiSyndicatedArticle; + title?: string; + topImageUrl?: string; + collection?: ClientApiCollection; + authors?: ClientApiAuthor[]; +}; + +export type UrlMetadata = { + url: string; + imageUrl?: string; + publisher?: string; + domain?: string; + title?: string; + excerpt?: string; + language?: string; + isSyndicated?: boolean; + isCollection?: boolean; + // authors is a comma separated string + authors?: string; +}; diff --git a/packages/terraform-modules/.eslintrc.js b/packages/terraform-modules/.eslintrc.js new file mode 100644 index 00000000..89fef142 --- /dev/null +++ b/packages/terraform-modules/.eslintrc.js @@ -0,0 +1,3 @@ +module.exports = { + extends: ['custom/library'], +}; diff --git a/packages/terraform-modules/README.md b/packages/terraform-modules/README.md new file mode 100644 index 00000000..9f0e7cb8 --- /dev/null +++ b/packages/terraform-modules/README.md @@ -0,0 +1,74 @@ +# terraform-modules + +This is a collection of terraform typescript cdk modules +that @Pocket uses in various services. + +## Why? + +Our goals for this project are as follows: + +- To have as little infrastructure code as possible in repos. +- To have as little *duplicated* infrastructure code as possible in repos. +- To provide reasonable defaults for our most common AWS services. +- To provide an as-simple-as-possible-while-still-configurable API to our most common AWS services by abstraction. + +## Repository Structure + +The bulk of the code for this repository is broken up into two folders: + +- `/src/base`: Contains abstractions of AWS services, e.g. an ECS or Redis Cluster, or an SQS Queue +- `/src/pocket`: Contains higher level abstractions that are specific to Pocket's infrastructure, e.g. an ALB-backed application or our PagerDuty config + +See the `README` file in each of these respective directories to learn more. (Coming soon.) + +## Testing + +### Snapshot Testing + +Snapshot testing ensures that our components produce predictable output when synthesized. + +When a snapshot test is first run, it generates a snapshot (in a `__snapshots__` directory) that is used to compare future synthesizing against a known, expected output. If a component changes, it's likely that the expected snapshot should change. If you are making a PR that changes a component, you should also update the related snapshot file. This can be done by running the test command with additional flags instructing Jest to re-build the snapshot based on the new state of the component: + +`npm test -- -u` + +The above will update any necessary snapshot files to be used on future test runs. + +As you can infer from the above, snapshots do not test the actual infrastructure result of running the synthesized terraform, meaning components should be tested manually (see below) to ensure they are performing the expected tasks prior to writing snapshot tests. + +### Testing in AWS + +While snapshot testing is great for things like regressions, it doesn't actually tell us if the code we've provided (e.g. the configuration of a particular AWS service) can be built in AWS. + +You can use the existing `example.ts` file to test the modules in this repo. + +1. Install [tfenv](https://github.com/tfutils/tfenv) +2. Run `tfenv use` to ensure you are on the same terraform version this repo is built for (defined in `.terraform-version`). +3. Run `npm install` +4. Run `npm run build:dev` +5. `cd` into the generated `cdktf.out/stacks/acme-example` directory +6. Run `terraform init` +7. Run `terraform validate` to validate the generated JSON (debugging level 1) + +To test against our infrastructure (debugging level 2): + +1. Log into terraform `terraform login` if not already. You will be prompted to save an API token. +2. Run `$(maws)` and select the 👉dev👈 backend SSO role (triple check that you are in DEV) +3. Run `terraform plan`, alternately in repo root run `cdktf plan` +4. Check one more time that you are in the dev account +5. Check with your teammates that it's okay to blow up the dev infra, then run `terraform apply`, alternately in repo root run `cdktf apply` +6. Clean up your mess by running `terraform destroy` when you're all done, alternately in repo root run `cdktf destroy` + +Note that this isn't a full end-to-end verification, and will hang on domain certificate steps, but the above should surface most generated terraform issues. + +### Testing in Dependent Repos + +Sometimes it is useful to develop the module while consuming it in another repo. + +1. Run `npm link` in the root of this repo +2. In the consumer repo run `npm link @pocket-tools/terraform-modules` +3. In this repo run `npm run watch` +4. Profit in the consumer repo - meaning, test deploying your application to AWS (probably in the dev account) + +When you are done be sure to: +1. `npm unlink` in this repo +2. `npm unlink @pocket-tools/terraform-modules` in the consuming repo diff --git a/packages/terraform-modules/cdktf.json b/packages/terraform-modules/cdktf.json new file mode 100644 index 00000000..b8b2b506 --- /dev/null +++ b/packages/terraform-modules/cdktf.json @@ -0,0 +1,5 @@ +{ + "language": "typescript", + "app": "npm run --silent compile && node dist/example.js", + "projectId": "67d4fb1a-0682-48af-bc23-a5ddc7941efc" +} \ No newline at end of file diff --git a/packages/terraform-modules/jest.config.js b/packages/terraform-modules/jest.config.js new file mode 100644 index 00000000..499e8269 --- /dev/null +++ b/packages/terraform-modules/jest.config.js @@ -0,0 +1,10 @@ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', + testMatch: ['**/?(*.)+(jest|spec).[jt]s?(x)'], + testPathIgnorePatterns: ['/dist/'], + clearMocks: true, + restoreMocks: true, + coverageProvider: 'v8', + setupFilesAfterEnv: ['./setup.js'], +}; diff --git a/packages/terraform-modules/package.json b/packages/terraform-modules/package.json new file mode 100644 index 00000000..2d3f1d82 --- /dev/null +++ b/packages/terraform-modules/package.json @@ -0,0 +1,40 @@ +{ + "name": "@pocket-tools/terraform-modules", + "version": "0.0.0-development", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "module": "dist/index.mjs", + "license": "MPL-2.0", + "scripts": { + "build:dev": "rm -rf dist && NODE_ENV=development npm run synth", + "build": "tsup src/index.ts --format cjs,esm --dts", + "synth": "cdktf synth", + "compile": "tsc --pretty", + "test:watch": "npm test -- --watch --watch-extensions ts -R min --watch-files src", + "test": "jest --ci --maxWorkers=4 --logHeapUsage", + "lint": "eslint --fix-dry-run \"src/**/*.ts\"", + "format": "eslint --fix \"src/**/*.ts\"", + "upgrade": "npm i cdktf@latest cdktf-cli@latest", + "upgrade:next": "npm i cdktf@next cdktf-cli@next" + }, + "dependencies": { + "@cdktf/provider-archive": "9.0.1", + "@cdktf/provider-aws": "18.2.0", + "@cdktf/provider-local": "9.0.1", + "@cdktf/provider-newrelic": "11.0.5", + "@cdktf/provider-null": "9.0.1", + "@cdktf/provider-pagerduty": "12.2.0", + "@cdktf/provider-time": "9.0.2", + "cdktf": "0.20.0-pre.70", + "cdktf-cli": "0.20.0-pre.70", + "constructs": "10.3.0", + "parse-domain": "5.0.0" + }, + "devDependencies": { + "tsconfig": "workspace:*", + "eslint-config-custom": "workspace:*", + "@types/jest": "29.5.11", + "jest": "29.7.0", + "ts-jest": "29.1.1" + } +} diff --git a/packages/terraform-modules/setup.js b/packages/terraform-modules/setup.js new file mode 100644 index 00000000..7c7c89a0 --- /dev/null +++ b/packages/terraform-modules/setup.js @@ -0,0 +1,2 @@ +const cdktf = require('cdktf'); +cdktf.Testing.setupJest(); diff --git a/packages/terraform-modules/src/base/ApplicationAutoscaling.spec.ts b/packages/terraform-modules/src/base/ApplicationAutoscaling.spec.ts new file mode 100644 index 00000000..ada3b594 --- /dev/null +++ b/packages/terraform-modules/src/base/ApplicationAutoscaling.spec.ts @@ -0,0 +1,139 @@ +import { Testing } from 'cdktf'; +import { TestResource } from '../testHelpers'; +import { + ApplicationAutoscaling, + ApplicationAutoscalingProps, +} from './ApplicationAutoscaling'; + +describe('ApplicationAutoscaling', () => { + const props: ApplicationAutoscalingProps = { + prefix: 'test-', + targetMinCapacity: 1, + targetMaxCapacity: 5, + ecsClusterName: 'ecs-cluster-test', + ecsServiceName: 'ecs-service-test', + scalableDimension: 'ecs:service:DesiredCount', + stepScaleOutAdjustment: 2, + stepScaleInAdjustment: -1, + scaleInThreshold: 30, + scaleOutThreshold: 45, + }; + + describe('generateAutoScalingTarget', () => { + it('renders an AutoscalingTarget', () => { + const synthed = Testing.synthScope((stack) => { + const construct = new TestResource(stack, 'test-resource'); + + ApplicationAutoscaling.generateAutoScalingTarget(construct, props); + }); + expect(synthed).toMatchSnapshot(); + }); + }); + + describe('generateAutoScalingPolicy', () => { + it('renders a scale-in AutoscalingPolicy', () => { + const synthed = Testing.synthScope((stack) => { + const construct = new TestResource(stack, 'test-resource'); + const target = ApplicationAutoscaling.generateAutoScalingTarget( + construct, + props, + ); + ApplicationAutoscaling.generateAutoSclaingPolicy( + construct, + props, + target, + 'In', + ); + }); + expect(synthed).toMatchSnapshot(); + }); + + it('renders a scale-out AutoscalingPolicy', () => { + const synthed = Testing.synthScope((stack) => { + const construct = new TestResource(stack, 'test-resource'); + const target = ApplicationAutoscaling.generateAutoScalingTarget( + construct, + props, + ); + ApplicationAutoscaling.generateAutoSclaingPolicy( + construct, + props, + target, + 'Out', + ); + }); + expect(synthed).toMatchSnapshot(); + }); + }); + + describe('generateCloudwatchMetricAlarm', () => { + it('renders a scale-in Cloudwatch Alarm', () => { + const synthed = Testing.synthScope((stack) => { + const construct = new TestResource(stack, 'test-resource'); + + const target = ApplicationAutoscaling.generateAutoScalingTarget( + construct, + props, + ); + + const policy = ApplicationAutoscaling.generateAutoSclaingPolicy( + construct, + props, + target, + 'In', + ); + + ApplicationAutoscaling.generateCloudwatchMetricAlarm( + construct, + props, + 'scale_in_alarm', + `${props.prefix} Service Low CPU`, + 'Alarm to reduce capacity if container CPU is low', + 'LessThanThreshold', + props.scaleInThreshold, + policy.arn, + ); + }); + expect(synthed).toMatchSnapshot(); + }); + + it('renders a scale-out Cloudwatch Alarm', () => { + const synthed = Testing.synthScope((stack) => { + const construct = new TestResource(stack, 'test-resource'); + + const target = ApplicationAutoscaling.generateAutoScalingTarget( + construct, + props, + ); + + const policy = ApplicationAutoscaling.generateAutoSclaingPolicy( + construct, + props, + target, + 'Out', + ); + + ApplicationAutoscaling.generateCloudwatchMetricAlarm( + construct, + props, + 'scale_out_alarm', + `${props.prefix} Service High CPU`, + 'Alarm to add capacity if container CPU is high', + 'GreaterThanThreshold', + props.scaleOutThreshold, + policy.arn, + ); + }); + expect(synthed).toMatchSnapshot(); + }); + }); + + describe('constructor', () => { + it('renders autoscaling without tags', () => { + const synthed = Testing.synthScope((stack) => { + new ApplicationAutoscaling(stack, 'testAutoscaling', props); + }); + expect(synthed).toMatchSnapshot(); + }); + }); +}); diff --git a/packages/terraform-modules/src/base/ApplicationAutoscaling.ts b/packages/terraform-modules/src/base/ApplicationAutoscaling.ts new file mode 100644 index 00000000..b566ae32 --- /dev/null +++ b/packages/terraform-modules/src/base/ApplicationAutoscaling.ts @@ -0,0 +1,202 @@ +import { TerraformMetaArguments } from 'cdktf'; +import { AppautoscalingPolicy } from '@cdktf/provider-aws/lib/appautoscaling-policy'; +import { AppautoscalingTarget } from '@cdktf/provider-aws/lib/appautoscaling-target'; +import { CloudwatchMetricAlarm } from '@cdktf/provider-aws/lib/cloudwatch-metric-alarm'; +import { Construct } from 'constructs'; + +export interface ApplicationAutoscalingProps extends TerraformMetaArguments { + ecsClusterName: string; + ecsServiceName: string; + prefix: string; + scalableDimension: string; + scaleInThreshold: number; + scaleOutThreshold: number; + stepScaleInAdjustment: number; + stepScaleOutAdjustment: number; + tags?: { [key: string]: string }; + targetMaxCapacity: number; + targetMinCapacity: number; +} + +/* + * Generates an AutoScaling group + */ + +export class ApplicationAutoscaling extends Construct { + constructor( + scope: Construct, + name: string, + config: ApplicationAutoscalingProps, + ) { + super(scope, name); + + // set up autoscaling target & in/out policies + const autoScalingTarget = ApplicationAutoscaling.generateAutoScalingTarget( + this, + config, + ); + + const applicationScaleOut = + ApplicationAutoscaling.generateAutoSclaingPolicy( + this, + config, + autoScalingTarget, + 'Out', + ); + + const applicationScaleIn = ApplicationAutoscaling.generateAutoSclaingPolicy( + this, + config, + autoScalingTarget, + 'In', + ); + + // set up cloudwatch alarms + ApplicationAutoscaling.generateCloudwatchMetricAlarm( + this, + config, + 'scale_out_alarm', + `${config.prefix} Service High CPU`, + 'Alarm to add capacity if container CPU is high', + 'GreaterThanThreshold', + config.scaleOutThreshold, + applicationScaleOut.arn, + ); + + ApplicationAutoscaling.generateCloudwatchMetricAlarm( + this, + config, + 'scale_in_alarm', + `${config.prefix} Service Low CPU`, + 'Alarm to reduce capacity if container CPU is low', + 'LessThanThreshold', + config.scaleInThreshold, + applicationScaleIn.arn, + ); + } + + /** + * Creates an Auto Scaling Target + * @param resource + * @param config + * @returns AppautoscalingTarget + */ + static generateAutoScalingTarget( + scope: Construct, + config: ApplicationAutoscalingProps, + ): AppautoscalingTarget { + return new AppautoscalingTarget(scope, `autoscaling_target`, { + maxCapacity: config.targetMaxCapacity, + minCapacity: config.targetMinCapacity, + resourceId: `service/${config.ecsClusterName}/${config.ecsServiceName}`, + scalableDimension: 'ecs:service:DesiredCount', + serviceNamespace: 'ecs', + provider: config.provider, + }); + } + + /** + * Creates an Autoscaling Policy + * @param resource + * @param config + * @param target + * @param type + * @returns AppautoscalingPolicy + */ + static generateAutoSclaingPolicy( + scope: Construct, + config: ApplicationAutoscalingProps, + target: AppautoscalingTarget, + type: 'In' | 'Out', + ): AppautoscalingPolicy { + let stepAdjustment; + + if (type === 'In') { + stepAdjustment = [ + { + metricIntervalUpperBound: '0', + scalingAdjustment: config.stepScaleInAdjustment, + }, + ]; + } else { + stepAdjustment = [ + { + metricIntervalLowerBound: '0', + scalingAdjustment: config.stepScaleOutAdjustment, + }, + ]; + } + + const appAutoscaling = new AppautoscalingPolicy( + scope, + `scale_${type.toLowerCase()}_policy`, + { + name: `${config.prefix}-Scale${type}Policy`, + policyType: 'StepScaling', + resourceId: target.resourceId, + scalableDimension: target.scalableDimension, + serviceNamespace: target.serviceNamespace, + + stepScalingPolicyConfiguration: { + adjustmentType: `ChangeInCapacity`, + cooldown: 60, + metricAggregationType: 'Average', + stepAdjustment, + }, + dependsOn: [target], + provider: config.provider, + }, + ); + + // Terraform CDK 0.8.1 started outputing this as a {} in syntiesized output and + // terraform does not like this being an empty object, but it is ok with a null + appAutoscaling.addOverride( + 'target_tracking_scaling_policy_configuration', + null, + ); + + return appAutoscaling; + } + + /** + * Creates a Cloudwatch Metric Alarm + * @param resource + * @param config + * @param id + * @param name + * @param desc + * @param operator + * @param threshold + * @param arn + */ + static generateCloudwatchMetricAlarm( + scope: Construct, + config: ApplicationAutoscalingProps, + id: string, + name: string, + desc: string, + operator: string, + threshold: number, + arn: string, + ): void { + new CloudwatchMetricAlarm(scope, id, { + alarmName: name, + alarmDescription: desc, + comparisonOperator: operator, + evaluationPeriods: 2, + threshold, + statistic: 'Average', + period: 60, + namespace: 'AWS/ECS', + metricName: 'CPUUtilization', + treatMissingData: 'notBreaching', + dimensions: { + ClusterName: config.ecsClusterName, + ServiceName: config.ecsServiceName, + }, + alarmActions: [arn], + tags: config.tags, + provider: config.provider, + }); + } +} diff --git a/packages/terraform-modules/src/base/ApplicationBackups.spec.ts b/packages/terraform-modules/src/base/ApplicationBackups.spec.ts new file mode 100644 index 00000000..b7ed7765 --- /dev/null +++ b/packages/terraform-modules/src/base/ApplicationBackups.spec.ts @@ -0,0 +1,74 @@ +import { Testing } from 'cdktf'; +import { ApplicationBackup } from './ApplicationBackups'; + +describe('ApplicationBackup', () => { + it('renders vault with plans without tags', () => { + const synthed = Testing.synthScope((stack) => { + new ApplicationBackup(stack, 'testBackup', { + name: 'name', + kmsKeyArn: 'arn:aws:kms:us-east-1:1234567890:key/mrk-1234', + prefix: 'prefix', + accountId: '1234567890', + vaultPolicy: + '{"Version": "2012-10-17","Statement": [{"Effect": "Allow","Action": "backup:CopyIntoBackupVault","Resource": "*","Principal": "*","Condition": {"StringEquals": {"aws:PrincipalOrgID": ["o-1234567890"]}}}]}', + backupPlans: [ + { + name: 'TestPlan', + resources: ['arn:aws:rds:us-east-1:123456790:db:test'], + rules: [ + { + ruleName: 'TestBackupRule', + schedule: 'cron( 0 5 ? * * *)', + }, + ], + selectionTag: [ + { + key: 'backups', + type: 'STRINGEQUALS', + value: 'True', + }, + ], + }, + ], + }); + }); + expect(synthed).toMatchSnapshot(); + }); + + it('renders vault with plans with tags', () => { + const synthed = Testing.synthScope((stack) => { + new ApplicationBackup(stack, 'testBackup', { + name: 'name', + kmsKeyArn: 'arn:aws:kms:us-east-1:1234567890:key/mrk-1234', + prefix: 'prefix', + accountId: '1234567890', + vaultPolicy: + '{"Version": "2012-10-17","Statement": [{"Effect": "Allow","Action": "backup:CopyIntoBackupVault","Resource": "*","Principal": "*","Condition": {"StringEquals": {"aws:PrincipalOrgID": ["o-1234567890"]}}}]}', + backupPlans: [ + { + name: 'TestPlan', + resources: ['arn:aws:rds:us-east-1:123456790:db:test'], + rules: [ + { + ruleName: 'TestBackupRule', + schedule: 'cron( 0 5 ? * * *)', + }, + ], + selectionTag: [ + { + key: 'backups', + type: 'STRINGEQUALS', + value: 'True', + }, + ], + }, + ], + tags: { + name: 'thedude', + hobby: 'bowling', + }, + }); + }); + expect(synthed).toMatchSnapshot(); + }); +}); diff --git a/packages/terraform-modules/src/base/ApplicationBackups.ts b/packages/terraform-modules/src/base/ApplicationBackups.ts new file mode 100644 index 00000000..30f9936a --- /dev/null +++ b/packages/terraform-modules/src/base/ApplicationBackups.ts @@ -0,0 +1,76 @@ +import { Construct } from 'constructs'; +import { TerraformMetaArguments } from 'cdktf'; +import { + BackupPlanRule, + BackupPlan, +} from '@cdktf/provider-aws/lib/backup-plan'; +import { + BackupSelection, + BackupSelectionSelectionTag, +} from '@cdktf/provider-aws/lib/backup-selection'; +import { BackupVault } from '@cdktf/provider-aws/lib/backup-vault'; +import { BackupVaultPolicy } from '@cdktf/provider-aws/lib/backup-vault-policy'; + +export interface ApplicationBackupProps extends TerraformMetaArguments { + name: string; + kmsKeyArn: string; + prefix: string; + accountId: string; + vaultPolicy: string; + backupPlans: { + name: string; + resources: string[]; + rules: Omit[]; + selectionTag: BackupSelectionSelectionTag[]; + }[]; + tags?: { [key: string]: string }; +} + +export class ApplicationBackup extends Construct { + public backupPlan: BackupPlan; + public backupSelection: BackupSelection; + public backupPlanRule: BackupPlanRule; + private static vault: BackupVault; + + constructor( + scope: Construct, + name: string, + private config: ApplicationBackupProps, + ) { + super(scope, name); + + const vault = new BackupVault(this, 'backup-vault', { + name: `${config.prefix}-${config.name}`, + kmsKeyArn: config.kmsKeyArn, + tags: config.tags, + provider: config.provider, + }); + + new BackupVaultPolicy(this, 'backup-vault-policy', { + backupVaultName: vault.name, + policy: config.vaultPolicy, + provider: config.provider, + }); + + config.backupPlans.forEach((plan) => { + const backupPlan = new BackupPlan(this, 'backup-plan', { + name: plan.name, + rule: plan.rules.map((rule) => ({ + ...rule, + targetVaultName: vault.name, + })), + tags: config.tags, + provider: config.provider, + }); + + new BackupSelection(this, 'backup-selection', { + name: `${config.prefix}-Backup-Selection`, + planId: backupPlan.id, + iamRoleArn: `arn:aws:iam::${config.accountId}:role/service-role/AWSBackupDefaultServiceRole`, + resources: plan.resources, + selectionTag: plan.selectionTag, + provider: config.provider, + }); + }); + } +} diff --git a/packages/terraform-modules/src/base/ApplicationBaseDNS.spec.ts b/packages/terraform-modules/src/base/ApplicationBaseDNS.spec.ts new file mode 100644 index 00000000..d4e441cc --- /dev/null +++ b/packages/terraform-modules/src/base/ApplicationBaseDNS.spec.ts @@ -0,0 +1,81 @@ +import { Testing } from 'cdktf'; +import { TestResource } from '../testHelpers'; +import { ApplicationBaseDNS } from './ApplicationBaseDNS'; + +describe('ApplicationBaseDNS', () => { + const tags = { + name: 'thedude', + hobby: 'bowling', + }; + + describe('retrieveAwsRoute53Zone', () => { + it('retrieves a route 53 zone', () => { + const synthed = Testing.synthScope((stack) => { + ApplicationBaseDNS.retrieveAwsRoute53Zone( + new TestResource(stack, 'test-resource'), + 'some-zone', + 'dev.gobowling.info', + ); + }); + expect(synthed).toMatchSnapshot(); + }); + }); + + describe('generateRoute53Zone', () => { + it('renders a route 53 zone', () => { + const synthed = Testing.synthScope((stack) => { + ApplicationBaseDNS.generateRoute53Zone( + new TestResource(stack, 'test-resource'), + 'dev.gobowling.info', + ); + }); + expect(synthed).toMatchSnapshot(); + }); + + it('renders a route 53 zone with tags', () => { + const synthed = Testing.synthScope((stack) => { + ApplicationBaseDNS.generateRoute53Zone( + new TestResource(stack, 'test-resource'), + 'dev.gobowling.info', + tags, + ); + }); + expect(synthed).toMatchSnapshot(); + }); + }); + + describe('generateRoute53Record', () => { + it('renders a route 53 record', () => { + const synthed = Testing.synthScope((stack) => { + ApplicationBaseDNS.generateRoute53Record( + new TestResource(stack, 'test-resource'), + 'dev.gobowling.info', + 'some-zone-id', + ['some', 'records'], + ); + }); + expect(synthed).toMatchSnapshot(); + }); + }); + + describe('constructor', () => { + it('renders base DNS without tags', () => { + const synthed = Testing.synthScope((stack) => { + new ApplicationBaseDNS(stack, 'testDNS', { + domain: 'dev.gobowling.info', + }); + }); + expect(synthed).toMatchSnapshot(); + }); + + it('renders base DNS with tags', () => { + const synthed = Testing.synthScope((stack) => { + new ApplicationBaseDNS(stack, 'testDNS', { + domain: 'dev.gobowling.info', + tags: tags, + }); + }); + expect(synthed).toMatchSnapshot(); + }); + }); +}); diff --git a/packages/terraform-modules/src/base/ApplicationBaseDNS.ts b/packages/terraform-modules/src/base/ApplicationBaseDNS.ts new file mode 100644 index 00000000..093225bc --- /dev/null +++ b/packages/terraform-modules/src/base/ApplicationBaseDNS.ts @@ -0,0 +1,85 @@ +import { DataAwsRoute53Zone } from '@cdktf/provider-aws/lib/data-aws-route53-zone'; +import { Route53Record } from '@cdktf/provider-aws/lib/route53-record'; +import { Route53Zone } from '@cdktf/provider-aws/lib/route53-zone'; +import { TerraformMetaArguments, TerraformProvider } from 'cdktf'; +import { Construct } from 'constructs'; +import { getRootDomain } from '../utilities'; + +export interface RootDNSProps extends TerraformMetaArguments { + domain: string; + tags?: { [key: string]: string }; +} + +export class ApplicationBaseDNS extends Construct { + public readonly zoneId: string; + + constructor(scope: Construct, name: string, config: RootDNSProps) { + super(scope, name); + + const route53MainZone = ApplicationBaseDNS.retrieveAwsRoute53Zone( + scope, + name, + config.domain, + config.provider, + ); + + const route53SubZone = ApplicationBaseDNS.generateRoute53Zone( + this, + config.domain, + config.tags, + config.provider, + ); + + this.zoneId = route53SubZone.zoneId; + + ApplicationBaseDNS.generateRoute53Record( + this, + config.domain, + route53MainZone.zoneId, + route53SubZone.nameServers, + config.provider, + ); + } + + static retrieveAwsRoute53Zone( + scope: Construct, + name: string, + domain: string, + provider?: TerraformProvider, + ): DataAwsRoute53Zone { + return new DataAwsRoute53Zone(scope, `${name}_main_hosted_zone`, { + name: getRootDomain(domain), + provider: provider, + }); + } + + static generateRoute53Zone( + scope: Construct, + domain: string, + tags?: { [key: string]: string }, + provider?: TerraformProvider, + ): Route53Zone { + return new Route53Zone(scope, `subhosted_zone`, { + name: domain, + tags: tags, + provider: provider, + }); + } + + static generateRoute53Record( + scope: Construct, + name: string, + zoneId: string, + records: string[], + provider?: TerraformProvider, + ): void { + new Route53Record(scope, `subhosted_zone_ns`, { + name, + type: 'NS', + ttl: 86400, + zoneId, + records, + provider: provider, + }); + } +} diff --git a/packages/terraform-modules/src/base/ApplicationCertificate.spec.ts b/packages/terraform-modules/src/base/ApplicationCertificate.spec.ts new file mode 100644 index 00000000..22493bd3 --- /dev/null +++ b/packages/terraform-modules/src/base/ApplicationCertificate.spec.ts @@ -0,0 +1,126 @@ +import { Testing, TerraformStack } from 'cdktf'; +import { TestResource } from '../testHelpers'; +import { ApplicationCertificate } from './ApplicationCertificate'; + +describe('ApplicationCertificate', () => { + const tags = { + name: 'thedude', + hobby: 'bowling', + }; + + describe('generateAcmCertificate', () => { + it('renders an acm certificate without tags', () => { + const synthed = Testing.synthScope((stack) => { + ApplicationCertificate.generateAcmCertificate( + new TestResource(stack, 'test-resource'), + 'dev.gobowling.info', + ); + }); + expect(synthed).toMatchSnapshot(); + }); + + it('renders an acm certificate with tags', () => { + const synthed = Testing.synthScope((stack) => { + ApplicationCertificate.generateAcmCertificate( + new TestResource(stack, 'test-resource'), + 'dev.gobowling.info', + tags, + ); + }); + expect(synthed).toMatchSnapshot(); + }); + }); + + describe('generateRoute53Record', () => { + it('renders a route 53 record', () => { + const synthed = Testing.synthScope((stack) => { + const construct = new TestResource(stack, 'test-resource'); + + const cert = ApplicationCertificate.generateAcmCertificate( + construct, + 'dev.gobowling.info', + ); + + ApplicationCertificate.generateRoute53Record( + construct, + 'dev.gobowling.info', + cert, + ); + }); + expect(synthed).toMatchSnapshot(); + }); + }); + + describe('generateAcmCertificateValidation', () => { + it('renders an acm certificate validation', () => { + const synthed = Testing.synthScope((stack) => { + const construct = new TestResource(stack, 'test-resource'); + + const cert = ApplicationCertificate.generateAcmCertificate( + construct, + 'dev.gobowling.info', + ); + + const record = ApplicationCertificate.generateRoute53Record( + construct, + 'dev.gobowling.info', + cert, + ); + + ApplicationCertificate.generateAcmCertificateValidation( + construct, + cert, + record, + ); + }); + expect(synthed).toMatchSnapshot(); + }); + }); + + describe('constructor', () => { + it('throws an error without a zone id or zone domain', () => { + const app = Testing.app(); + const stack = new TerraformStack(app, 'test'); + + expect(() => { + new ApplicationCertificate(stack, 'testCert', { + domain: 'dev.gobowling.info', + }); + }).toThrow('You need to pass either a zone id or a zone domain'); + }); + + it('renders a cert with a zone id', () => { + const synthed = Testing.synthScope((stack) => { + new ApplicationCertificate(stack, 'testCert', { + domain: 'dev.gobowling.info', + zoneId: 'malibu', + }); + }); + expect(synthed).toMatchSnapshot(); + }); + + it('renders a cert with a zone domain', () => { + const synthed = Testing.synthScope((stack) => { + new ApplicationCertificate(stack, 'testCert', { + domain: 'dev.gobowling.info', + zoneId: 'gobowling.info', + }); + }); + expect(synthed).toMatchSnapshot(); + }); + + it('renders a cert with tags', () => { + const synthed = Testing.synthScope((stack) => { + new ApplicationCertificate(stack, 'testCert', { + domain: 'dev.gobowling.info', + zoneId: 'gobowling.info', + tags: { + name: 'thedude', + hobby: 'bowling', + }, + }); + }); + expect(synthed).toMatchSnapshot(); + }); + }); +}); diff --git a/packages/terraform-modules/src/base/ApplicationCertificate.ts b/packages/terraform-modules/src/base/ApplicationCertificate.ts new file mode 100644 index 00000000..7dee7c0d --- /dev/null +++ b/packages/terraform-modules/src/base/ApplicationCertificate.ts @@ -0,0 +1,118 @@ +import { AcmCertificate } from '@cdktf/provider-aws/lib/acm-certificate'; +import { AcmCertificateValidation } from '@cdktf/provider-aws/lib/acm-certificate-validation'; +import { DataAwsRoute53Zone } from '@cdktf/provider-aws/lib/data-aws-route53-zone'; +import { Route53Record } from '@cdktf/provider-aws/lib/route53-record'; +import { TerraformMetaArguments, TerraformProvider } from 'cdktf'; +import { Construct } from 'constructs'; + +export interface ApplicationCertificateProps extends TerraformMetaArguments { + domain: string; + /** + * If zoneId is not passed then we use a data block and the zoneDomain to grab it. + */ + zoneId?: string; + zoneDomain?: string; + tags?: { [key: string]: string }; +} + +/** + * Generates an Application Certificate given a domain name and zoneId + */ +export class ApplicationCertificate extends Construct { + public readonly arn: string; + // Use `certificateValidation` in `dependsOn` to block on the + // complete certificate for any downstream dependencies + public readonly certificateValidation: AcmCertificateValidation; + + constructor( + scope: Construct, + name: string, + config: ApplicationCertificateProps, + ) { + super(scope, name); + + if (!config.zoneId && config.zoneDomain) { + const route53Zone = new DataAwsRoute53Zone(this, `zone`, { + name: config.zoneDomain, + provider: config.provider, + }); + config.zoneId = route53Zone.zoneId; + } else if (!config.zoneId && !config.zoneDomain) { + throw new Error('You need to pass either a zone id or a zone domain'); + } + + const certificate = ApplicationCertificate.generateAcmCertificate( + this, + config.domain, + config.tags, + config.provider, + ); + + const certificateRecord = ApplicationCertificate.generateRoute53Record( + this, + config.zoneId, + certificate, + config.provider, + ); + + const validation = ApplicationCertificate.generateAcmCertificateValidation( + this, + certificate, + certificateRecord, + config.provider, + ); + + this.arn = certificate.arn; + this.certificateValidation = validation; + } + + static generateAcmCertificate( + scope: Construct, + domain: string, + tags?: { [key: string]: string }, + provider?: TerraformProvider, + ): AcmCertificate { + return new AcmCertificate(scope, `certificate`, { + domainName: domain, + validationMethod: 'DNS', + tags: tags, + lifecycle: { + createBeforeDestroy: true, + }, + provider: provider, + }); + } + + static generateRoute53Record( + scope: Construct, + zoneId: string, + cert: AcmCertificate, + provider?: TerraformProvider, + ): Route53Record { + const record = new Route53Record(scope, `certificate_record`, { + name: cert.domainValidationOptions.get(0).resourceRecordName, + type: cert.domainValidationOptions.get(0).resourceRecordType, + zoneId, + records: [cert.domainValidationOptions.get(0).resourceRecordValue], + ttl: 60, + dependsOn: [cert], + provider: provider, + }); + + return record; + } + + static generateAcmCertificateValidation( + scope: Construct, + cert: AcmCertificate, + record: Route53Record, + provider?: TerraformProvider, + ): AcmCertificateValidation { + return new AcmCertificateValidation(scope, `certificate_validation`, { + certificateArn: cert.arn, + validationRecordFqdns: [record.fqdn], + dependsOn: [record, cert], + provider: provider, + }); + } +} diff --git a/packages/terraform-modules/src/base/ApplicationDynamoDBTable.spec.ts b/packages/terraform-modules/src/base/ApplicationDynamoDBTable.spec.ts new file mode 100644 index 00000000..f0c485f7 --- /dev/null +++ b/packages/terraform-modules/src/base/ApplicationDynamoDBTable.spec.ts @@ -0,0 +1,239 @@ +import { DynamodbTableGlobalSecondaryIndex } from '@cdktf/provider-aws/lib/dynamodb-table'; +import { Testing } from 'cdktf'; +import { + ApplicationDynamoDBTable, + ApplicationDynamoDBProps, + ApplicationDynamoDBTableCapacityMode, + ApplicationDynamoDBTableStreamViewType, +} from './ApplicationDynamoDBTable'; + +describe('ApplicationDynamoDBTable', () => { + let BASE_CONFIG: ApplicationDynamoDBProps; + + beforeEach(() => { + BASE_CONFIG = { + prefix: 'abides-', + tableConfig: { + hashKey: '123', + attribute: [ + { + name: 'attribeautiful', + type: 'shrugs!', + }, + ], + globalSecondaryIndex: [], + }, + }; + }); + + it('renders dynamo db table with minimal config', () => { + const synthed = Testing.synthScope((stack) => { + new ApplicationDynamoDBTable(stack, 'testDynamoDBTable', BASE_CONFIG); + }); + expect(synthed).toMatchSnapshot(); + }); + + it('renders dynamo db table with read capacity', () => { + BASE_CONFIG.readCapacity = { + tracking: 1, + max: 10, + min: 3, + }; + + const synthed = Testing.synthScope((stack) => { + new ApplicationDynamoDBTable(stack, 'testDynamoDBTable', BASE_CONFIG); + }); + expect(synthed).toMatchSnapshot(); + }); + + it('renders dynamo db table with write capacity', () => { + BASE_CONFIG.writeCapacity = { + tracking: 1, + max: 10, + min: 3, + }; + + const synthed = Testing.synthScope((stack) => { + new ApplicationDynamoDBTable(stack, 'testDynamoDBTable', BASE_CONFIG); + }); + expect(synthed).toMatchSnapshot(); + }); + + it('renders dynamo db table with read and write capacity', () => { + BASE_CONFIG.writeCapacity = { + tracking: 1, + max: 10, + min: 3, + }; + + BASE_CONFIG.readCapacity = { + tracking: 1, + max: 10, + min: 3, + }; + + const synthed = Testing.synthScope((stack) => { + new ApplicationDynamoDBTable(stack, 'testDynamoDBTable', BASE_CONFIG); + }); + expect(synthed).toMatchSnapshot(); + }); + + it('renders dynamo db table with read and write capacity and tags', () => { + BASE_CONFIG.writeCapacity = { + tracking: 1, + max: 10, + min: 3, + }; + + BASE_CONFIG.readCapacity = { + tracking: 1, + max: 10, + min: 3, + }; + + BASE_CONFIG.tags = { + name: 'thedude', + hobby: 'bowling', + }; + + const synthed = Testing.synthScope((stack) => { + new ApplicationDynamoDBTable(stack, 'testDynamoDBTable', BASE_CONFIG); + }); + expect(synthed).toMatchSnapshot(); + }); + + it('renders dynamo db table global secondary indexes', () => { + BASE_CONFIG.writeCapacity = { + tracking: 1, + max: 10, + min: 3, + }; + + BASE_CONFIG.readCapacity = { + tracking: 1, + max: 10, + min: 3, + }; + + ( + BASE_CONFIG.tableConfig + .globalSecondaryIndex as DynamodbTableGlobalSecondaryIndex[] + ).push({ + name: 'card-index', + hashKey: 'card-type', + rangeKey: 'home_on_the_range', + projectionType: 'ALL', + readCapacity: 5, + writeCapacity: 5, + }); + + const synthed = Testing.synthScope((stack) => { + new ApplicationDynamoDBTable(stack, 'testDynamoDBTable', BASE_CONFIG); + }); + expect(synthed).toMatchSnapshot(); + }); + + it('renders dynamo db table with 2 global secondary indexes', () => { + BASE_CONFIG.writeCapacity = { + tracking: 1, + max: 10, + min: 3, + }; + + BASE_CONFIG.readCapacity = { + tracking: 1, + max: 10, + min: 3, + }; + + //This test runs after the first secondary index test, so here we just add another index which gives us 2 + ( + BASE_CONFIG.tableConfig + .globalSecondaryIndex as DynamodbTableGlobalSecondaryIndex[] + ).push({ + name: 'card-index-2', + hashKey: 'card-type-123', + rangeKey: 'home_home_on_the_range', + projectionType: 'ALL', + readCapacity: 10, + writeCapacity: 10, + }); + + const synthed = Testing.synthScope((stack) => { + new ApplicationDynamoDBTable(stack, 'testDynamoDBTable', BASE_CONFIG); + }); + expect(synthed).toMatchSnapshot(); + }); + + it('renders dynamo db table that is not protected from being destroyed', () => { + BASE_CONFIG.preventDestroyTable = false; + + const synthed = Testing.synthScope((stack) => { + const applicationDynamoDBTable = new ApplicationDynamoDBTable( + stack, + 'testDynamoDBTable', + BASE_CONFIG, + ); + + expect( + applicationDynamoDBTable.dynamodb.lifecycle.preventDestroy, + ).toEqual(false); + }); + expect(synthed).toMatchSnapshot(); + }); + + it('renders dynamo db table with on-demand capacity', () => { + BASE_CONFIG.capacityMode = ApplicationDynamoDBTableCapacityMode.ON_DEMAND; + const synthed = Testing.synthScope((stack) => { + new ApplicationDynamoDBTable(stack, 'testDynamoDBTable', BASE_CONFIG); + }); + expect(synthed).toMatchSnapshot(); + }); + + it('renders a dynamodb table with streams enabled', () => { + const config: ApplicationDynamoDBProps = { + ...BASE_CONFIG, + tableConfig: { + streamEnabled: true, + streamViewType: + ApplicationDynamoDBTableStreamViewType.NEW_AND_OLD_IMAGES, + }, + }; + + const synthed = Testing.synthScope((stack) => { + new ApplicationDynamoDBTable(stack, 'testDynamoDBTable', config); + }); + expect(synthed).toMatchSnapshot(); + }); + + it('fails to render a dynamodb table with streams enabled if stream view type is not specified', () => { + const config: ApplicationDynamoDBProps = { + ...BASE_CONFIG, + tableConfig: { + streamEnabled: true, + }, + }; + + expect(() => { + Testing.synthScope((stack) => { + new ApplicationDynamoDBTable(stack, 'testDynamoDBTable', config); + }); + }).toThrow('you must specify a stream view type if streams are enabled'); + }); + + it('fails to render a dynamodb table with streams enabled if stream view type is invalid', () => { + const config: ApplicationDynamoDBProps = { + ...BASE_CONFIG, + tableConfig: { + streamEnabled: true, + streamViewType: 'PURPLE_MONKEY_DISHWASHER', + }, + }; + + expect(() => { + Testing.synthScope((stack) => { + new ApplicationDynamoDBTable(stack, 'testDynamoDBTable', config); + }); + }).toThrow('you must specify a valid stream view type'); + }); +}); diff --git a/packages/terraform-modules/src/base/ApplicationDynamoDBTable.ts b/packages/terraform-modules/src/base/ApplicationDynamoDBTable.ts new file mode 100644 index 00000000..40deb622 --- /dev/null +++ b/packages/terraform-modules/src/base/ApplicationDynamoDBTable.ts @@ -0,0 +1,391 @@ +import { AppautoscalingPolicy } from '@cdktf/provider-aws/lib/appautoscaling-policy'; +import { AppautoscalingTarget } from '@cdktf/provider-aws/lib/appautoscaling-target'; +import { DataAwsIamPolicyDocument } from '@cdktf/provider-aws/lib/data-aws-iam-policy-document'; +import { + DynamodbTableConfig, + DynamodbTable, + DynamodbTableGlobalSecondaryIndex, +} from '@cdktf/provider-aws/lib/dynamodb-table'; +import { IamPolicy } from '@cdktf/provider-aws/lib/iam-policy'; +import { IamRole } from '@cdktf/provider-aws/lib/iam-role'; +import { IamRolePolicyAttachment } from '@cdktf/provider-aws/lib/iam-role-policy-attachment'; +import { IResolvable, TerraformMetaArguments, TerraformProvider } from 'cdktf'; +import { Construct } from 'constructs'; + +/** + * Enum to determine the capacity type for autoscaling + */ +export enum ApplicationDynamoDBTableCapacityType { + Read = 'ReadCapacity', + Write = 'WriteCapacity', +} + +export enum ApplicationDynamoDBTableCapacityMode { + PROVISIONED = 'PROVISIONED', + ON_DEMAND = 'PAY_PER_REQUEST', // Confusingly, on-demand is called "PAY_PER_REQUEST" in TF and CloudFormation. +} + +export enum ApplicationDynamoDBTableStreamViewType { + KEYS_ONLY = 'KEYS_ONLY', + NEW_IMAGE = 'NEW_IMAGE', + OLD_IMAGE = 'OLD_IMAGE', + NEW_AND_OLD_IMAGES = 'NEW_AND_OLD_IMAGES', +} + +export interface ApplicationDynamoDBTableAutoScaleProps { + tracking: number; + max: number; + min: number; +} + +//Override the default dynamo config but remove the items that we set ourselves. +export type ApplicationDynamoDBTableConfig = Omit< + DynamodbTableConfig, + 'name' | 'tags' | 'lifecycle' +>; + +export interface ApplicationDynamoDBProps extends TerraformMetaArguments { + tags?: { [key: string]: string }; + prefix: string; + tableConfig: ApplicationDynamoDBTableConfig; + readCapacity?: ApplicationDynamoDBTableAutoScaleProps; + writeCapacity?: ApplicationDynamoDBTableAutoScaleProps; + // If capacityMode is ON_DEMAND, the dynamodb table will have on-demand capacity. By default this is PROVISIONED. + // On-demand capacity mode is capable of serving thousands of requests per second without capacity planning. + // https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/HowItWorks.ReadWriteCapacityMode.html + // The readCapacity and writeCapacity properties should not be used if capacityMode is set to ON_DEMAND. + capacityMode?: ApplicationDynamoDBTableCapacityMode; + // If true, the dynamodb table will be protected from being destroyed. Enabled by default. + preventDestroyTable?: boolean; +} + +/** + * Generates a dynamodb + */ +export class ApplicationDynamoDBTable extends Construct { + public readonly dynamodb: DynamodbTable; + + constructor( + scope: Construct, + name: string, + config: ApplicationDynamoDBProps, + ) { + super(scope, name); + + // validate stream config (if enabled) + ApplicationDynamoDBTable.validateStreamConfig( + config.tableConfig, + ApplicationDynamoDBTableStreamViewType, + ); + + const billingMode: string = ( + config.capacityMode ?? ApplicationDynamoDBTableCapacityMode.PROVISIONED + ).valueOf(); + + this.dynamodb = new DynamodbTable(this, `dynamodb_table`, { + ...config.tableConfig, + billingMode: billingMode, + tags: config.tags, + name: config.prefix, + lifecycle: { + ignoreChanges: ['read_capacity', 'write_capacity'], + // Protect the table from being removed, unless preventDestroyTable is explicitly set to false. + preventDestroy: config.preventDestroyTable !== false, + }, + provider: config.provider, + }); + + if (config.readCapacity) { + ApplicationDynamoDBTable.setupAutoscaling( + this, + config.prefix, + config.readCapacity, + this.dynamodb, + ApplicationDynamoDBTableCapacityType.Read, + config.tableConfig.globalSecondaryIndex, + config.tags, + config.provider, + ); + } + + if (config.writeCapacity) { + ApplicationDynamoDBTable.setupAutoscaling( + this, + config.prefix, + config.writeCapacity, + this.dynamodb, + ApplicationDynamoDBTableCapacityType.Write, + config.tableConfig.globalSecondaryIndex, + config.tags, + config.provider, + ); + } + } + + /** + * Sets up autoscaling for dynamodb on a write or read target + * @param scope + * @param prefix + * @param config + * @param dynamoDB + * @param capacityType + * @param globalSecondaryIndexes + * @param tags + * @private + */ + private static setupAutoscaling( + scope: Construct, + prefix, + config: ApplicationDynamoDBTableAutoScaleProps, + dynamoDB: DynamodbTable, + capacityType: ApplicationDynamoDBTableCapacityType, + globalSecondaryIndexes: DynamodbTableGlobalSecondaryIndex[] | IResolvable, + tags?: { [key: string]: string }, + provider?: TerraformProvider, + ): void { + const roleArn = ApplicationDynamoDBTable.createAutoScalingRole( + scope, + capacityType, + prefix, + dynamoDB.arn, + tags, + provider, + ); + + // create an auto scaling policy for the table + ApplicationDynamoDBTable.createAutoScalingPolicy( + scope, + roleArn, + 'table', + capacityType, + config.min, + config.max, + config.tracking, + dynamoDB, + undefined, + provider, + ); + + //cdktf 0.9 updated the types of Globalsecondary indexes to be IResolvable | DynamodbGlobalSecondaryIndexes[] + // we need to cast it here to loop it. + const castedGlobalSecondaryIndexes = + globalSecondaryIndexes as DynamodbTableGlobalSecondaryIndex[]; + + // create an auto scaling policy for each global secondary index + if (castedGlobalSecondaryIndexes.length) { + castedGlobalSecondaryIndexes.forEach((gsIndex) => { + // min capacity is defined by the global secondary index + // max capacity is inherited from the table auto scaling config + // TODO: if we want this to be configurabe per index, we'll need to extend the third-party interface + const minCapacity = + capacityType === ApplicationDynamoDBTableCapacityType.Read + ? gsIndex.readCapacity + : gsIndex.writeCapacity; + + // create an auto scaling policy for each index + ApplicationDynamoDBTable.createAutoScalingPolicy( + scope, + roleArn, + 'index', + capacityType, + minCapacity, + config.max, + config.tracking, + dynamoDB, + gsIndex.name, + provider, + ); + }); + } + } + + /** + * sets up autoscaling policy for a table or an index + * @param scope + * @param roleArn + * @param policyTarget + * @param capacityType + * @param minCapacity + * @param maxCapacity + * @param tracking + * @param dynamoDB + * @param indexName + * @private + */ + private static createAutoScalingPolicy( + scope: Construct, + roleArn: string, + policyTarget: 'table' | 'index', + capacityType: ApplicationDynamoDBTableCapacityType, + minCapacity: number, + maxCapacity: number, + tracking: number, + dynamoDB: DynamodbTable, + indexName?: string, + provider?: TerraformProvider, + ): void { + let resourceId = `table/${dynamoDB.name}`; + + // if we're targeting an index, the resource id must reflect that + if (policyTarget === 'index') { + if (indexName) { + resourceId += `/index/${indexName}`; + } else { + throw new Error( + 'you must specify an indexName when creating an index auto scaling policy', + ); + } + } + + const constructPrefix = `${ + indexName ? indexName : dynamoDB.friendlyUniqueId + }_${capacityType}_${policyTarget}`; + + const targetTracking = new AppautoscalingTarget( + scope, + `${constructPrefix}_target`, + { + maxCapacity, + minCapacity, + resourceId, + scalableDimension: `dynamodb:${policyTarget}:${capacityType}Units`, + roleArn: roleArn, + serviceNamespace: 'dynamodb', + dependsOn: [dynamoDB], + provider, + }, + ); + + new AppautoscalingPolicy(scope, `${constructPrefix}_policy`, { + name: `DynamoDB${capacityType}Utilization:${targetTracking.resourceId}`, + policyType: 'TargetTrackingScaling', + resourceId: targetTracking.resourceId, + scalableDimension: targetTracking.scalableDimension, + serviceNamespace: targetTracking.serviceNamespace, + targetTrackingScalingPolicyConfiguration: { + predefinedMetricSpecification: { + predefinedMetricType: `DynamoDB${capacityType}Utilization`, + }, + targetValue: tracking, + }, + dependsOn: [targetTracking, dynamoDB], + provider, + }); + } + + /** + * Creates the autoscaling role necessary for DynamoDB + * @param scope + * @param capacityType + * @param prefix + * @param dynamoDBARN + * @param tags + * @private + */ + private static createAutoScalingRole( + scope: Construct, + capacityType: ApplicationDynamoDBTableCapacityType, + prefix: string, + dynamoDBARN: string, + tags?: { [key: string]: string }, + provider?: TerraformProvider, + ): string { + const policy = new IamPolicy(scope, `${capacityType}_autoscaling_policy`, { + name: `${prefix}-${capacityType}-AutoScalingPolicy`, + policy: new DataAwsIamPolicyDocument( + scope, + `${capacityType}_policy_document`, + { + statement: [ + { + effect: 'Allow', + actions: [ + 'application-autoscaling:*', + 'cloudwatch:DescribeAlarms', + 'cloudwatch:PutMetricAlarm', + ], + resources: ['*'], + }, + { + effect: 'Allow', + actions: ['dynamodb:DescribeTable', 'dynamodb:UpdateTable'], + resources: [dynamoDBARN, `${dynamoDBARN}*`], // 🏚 + }, + ], + }, + ).json, + provider, + }); + + // In a perfect world we would be using a IamServiceLinkedRole, but Amazon is very amazon. + // Amazon doesn't allow a custom suffix for dynamodb application autoscaling, so we need to use an IAM Role. + // The unfortunate piece is that Amazon will overwrite the role we set below with an account wide DynamoDB autoscale role. + // Hopefully one day we can fix this and limit the application autoscale role. But today is not that day + + // const role = new IamServiceLinkedRole(scope, `${capacityType}_role`, { + // awsServiceName: 'application-autoscaling.amazonaws.com', + // customSuffix: `${prefix}-${capacityType}`, + // description: `Autoscaling Service Role for ${prefix}-${capacityType}`, + // }); + + const role = new IamRole(scope, `${capacityType}_role`, { + name: `${prefix}-${capacityType}-AutoScalingRole`, + tags: tags, + assumeRolePolicy: new DataAwsIamPolicyDocument( + scope, + `${capacityType}_assume_role_policy_document`, + { + statement: [ + { + effect: 'Allow', + actions: ['sts:AssumeRole'], + principals: [ + { + type: 'Service', + identifiers: ['application-autoscaling.amazonaws.com'], + }, + ], + }, + ], + }, + ).json, + provider, + }); + + new IamRolePolicyAttachment(scope, `${capacityType}_role_attachment`, { + policyArn: policy.arn, + role: role.name, + dependsOn: [role, policy], + provider, + }); + + return role.arn; + } + + /** + * If streams are enabled, validates the stream view type is present and + * contains an expected value. + * @param tableConfig + * @param streamViewTypeValues + */ + private static validateStreamConfig( + tableConfig: ApplicationDynamoDBTableConfig, + streamViewTypeValues: typeof ApplicationDynamoDBTableStreamViewType, + ): void { + if (tableConfig.streamEnabled) { + if (!tableConfig.streamViewType) { + throw new Error( + 'you must specify a stream view type if streams are enabled', + ); + } + + if ( + !Object.values(streamViewTypeValues).includes( + tableConfig.streamViewType, + ) + ) { + throw new Error('you must specify a valid stream view type'); + } + } + } +} diff --git a/packages/terraform-modules/src/base/ApplicationECR.spec.ts b/packages/terraform-modules/src/base/ApplicationECR.spec.ts new file mode 100644 index 00000000..84af44bd --- /dev/null +++ b/packages/terraform-modules/src/base/ApplicationECR.spec.ts @@ -0,0 +1,26 @@ +import { Testing } from 'cdktf'; +import { ApplicationECR } from './ApplicationECR'; + +describe('ApplicationECR', () => { + it('renders an ECR without tags', () => { + const synthed = Testing.synthScope((stack) => { + new ApplicationECR(stack, 'testECR', { + name: 'bowling', + }); + }); + expect(synthed).toMatchSnapshot(); + }); + + it('renders an ECR with tags', () => { + const synthed = Testing.synthScope((stack) => { + new ApplicationECR(stack, 'testECR', { + name: 'bowling', + tags: { + name: 'rug', + description: 'tiedtheroomtogether', + }, + }); + }); + expect(synthed).toMatchSnapshot(); + }); +}); diff --git a/packages/terraform-modules/src/base/ApplicationECR.ts b/packages/terraform-modules/src/base/ApplicationECR.ts new file mode 100644 index 00000000..8d9292f4 --- /dev/null +++ b/packages/terraform-modules/src/base/ApplicationECR.ts @@ -0,0 +1,63 @@ +import { + EcrLifecyclePolicyConfig, + EcrLifecyclePolicy, +} from '@cdktf/provider-aws/lib/ecr-lifecycle-policy'; +import { + EcrRepository, + EcrRepositoryConfig, +} from '@cdktf/provider-aws/lib/ecr-repository'; +import { TerraformMetaArguments } from 'cdktf'; +import { Construct } from 'constructs'; + +export interface ECRProps extends TerraformMetaArguments { + name: string; + tags?: { [key: string]: string }; +} + +export class ApplicationECR extends Construct { + public readonly repo: EcrRepository; + + constructor(scope: Construct, name: string, config: ECRProps) { + super(scope, name); + + const ecrConfig: EcrRepositoryConfig = { + name: config.name, + tags: config.tags, + imageScanningConfiguration: { + scanOnPush: true, // scans docker image for vulnerabilities + }, + provider: config.provider, + }; + + this.repo = new EcrRepository(this, 'ecr-repo', ecrConfig); + + // this is our default policy + // perhaps this should be defined elsewhere? or allow to be overwritten? + // decisions for another day... + const policy = { + rules: [ + { + rulePriority: 1, + description: 'expire old images', + selection: { + tagStatus: 'any', + countType: 'imageCountMoreThan', + countNumber: 800, + }, + action: { + type: 'expire', + }, + }, + ], + }; + + const ecrPolicyConfig: EcrLifecyclePolicyConfig = { + repository: this.repo.name, + policy: JSON.stringify(policy), + dependsOn: [this.repo], + provider: config.provider, + }; + + new EcrLifecyclePolicy(this, 'ecr-repo-lifecyclepolicy', ecrPolicyConfig); + } +} diff --git a/packages/terraform-modules/src/base/ApplicationECSAlbCodeDeploy.spec.ts b/packages/terraform-modules/src/base/ApplicationECSAlbCodeDeploy.spec.ts new file mode 100644 index 00000000..5ec604ec --- /dev/null +++ b/packages/terraform-modules/src/base/ApplicationECSAlbCodeDeploy.spec.ts @@ -0,0 +1,141 @@ +import { Testing } from 'cdktf'; +import { ApplicationECSAlbCodeDeploy } from './ApplicationECSAlbCodeDeploy'; + +describe('ApplicationECSAlbCodeDeploy', () => { + it('renders a CodeDeploy without a sns', () => { + const synthed = Testing.synthScope((stack) => { + new ApplicationECSAlbCodeDeploy(stack, 'testCodeDeploy', { + prefix: 'Test-Dev', + clusterName: 'cluster', + serviceName: 'theService', + listenerArn: 'listen-to-me', + targetGroupNames: ['target-1', 'target-2'], + }); + }); + expect(synthed).toMatchSnapshot(); + }); + + it('renders a CodeDeploy with a sns', () => { + const synthed = Testing.synthScope((stack) => { + new ApplicationECSAlbCodeDeploy(stack, 'testCodeDeploy', { + prefix: 'Test-Dev', + clusterName: 'cluster', + serviceName: 'theService', + listenerArn: 'listen-to-me', + snsNotificationTopicArn: 'notify-me', + targetGroupNames: ['target-1', 'target-2'], + }); + }); + expect(synthed).toMatchSnapshot(); + }); + + it('renders a CodeDeploy with tags', () => { + const synthed = Testing.synthScope((stack) => { + new ApplicationECSAlbCodeDeploy(stack, 'testCodeDeploy', { + prefix: 'Test-Dev', + clusterName: 'cluster', + serviceName: 'theService', + listenerArn: 'listen-to-me', + snsNotificationTopicArn: 'notify-me', + targetGroupNames: ['target-1', 'target-2'], + tags: { + test: '1234', + tag: 'me', + }, + }); + }); + expect(synthed).toMatchSnapshot(); + }); + + it('renders a CodeDeploy with only failed deploy notifications', () => { + const synthed = Testing.synthScope((stack) => { + new ApplicationECSAlbCodeDeploy(stack, 'testCodeDeploy', { + prefix: 'Test-Dev', + clusterName: 'cluster', + serviceName: 'theService', + listenerArn: 'listen-to-me', + snsNotificationTopicArn: 'notify-me', + targetGroupNames: ['target-1', 'target-2'], + tags: { + test: '1234', + tag: 'me', + }, + notifications: { + notifyOnFailed: true, + notifyOnStarted: false, + notifyOnSucceeded: false, + }, + }); + }); + expect(synthed).toMatchSnapshot(); + }); + + it('renders a CodeDeploy with only succeeded deploy notifications', () => { + const synthed = Testing.synthScope((stack) => { + new ApplicationECSAlbCodeDeploy(stack, 'testCodeDeploy', { + prefix: 'Test-Dev', + clusterName: 'cluster', + serviceName: 'theService', + listenerArn: 'listen-to-me', + snsNotificationTopicArn: 'notify-me', + targetGroupNames: ['target-1', 'target-2'], + tags: { + test: '1234', + tag: 'me', + }, + notifications: { + notifyOnFailed: false, + notifyOnStarted: false, + notifyOnSucceeded: true, + }, + }); + }); + expect(synthed).toMatchSnapshot(); + }); + + it('renders a CodeDeploy with only started deploy notifications', () => { + const synthed = Testing.synthScope((stack) => { + new ApplicationECSAlbCodeDeploy(stack, 'testCodeDeploy', { + prefix: 'Test-Dev', + clusterName: 'cluster', + serviceName: 'theService', + listenerArn: 'listen-to-me', + snsNotificationTopicArn: 'notify-me', + targetGroupNames: ['target-1', 'target-2'], + tags: { + test: '1234', + tag: 'me', + }, + notifications: { + notifyOnFailed: false, + notifyOnStarted: true, + notifyOnSucceeded: false, + }, + }); + }); + expect(synthed).toMatchSnapshot(); + }); + + it('renders a CodeDeploy with no deploy notifications', () => { + const synthed = Testing.synthScope((stack) => { + new ApplicationECSAlbCodeDeploy(stack, 'testCodeDeploy', { + prefix: 'Test-Dev', + clusterName: 'cluster', + serviceName: 'theService', + listenerArn: 'listen-to-me', + snsNotificationTopicArn: 'notify-me', + targetGroupNames: ['target-1', 'target-2'], + tags: { + test: '1234', + tag: 'me', + }, + notifications: { + notifyOnFailed: false, + notifyOnStarted: false, + notifyOnSucceeded: false, + }, + }); + }); + expect(synthed).toMatchSnapshot(); + }); +}); diff --git a/packages/terraform-modules/src/base/ApplicationECSAlbCodeDeploy.ts b/packages/terraform-modules/src/base/ApplicationECSAlbCodeDeploy.ts new file mode 100644 index 00000000..58797ae2 --- /dev/null +++ b/packages/terraform-modules/src/base/ApplicationECSAlbCodeDeploy.ts @@ -0,0 +1,206 @@ +import { CodedeployApp } from '@cdktf/provider-aws/lib/codedeploy-app'; +import { CodedeployDeploymentGroup } from '@cdktf/provider-aws/lib/codedeploy-deployment-group'; +import { CodestarnotificationsNotificationRule } from '@cdktf/provider-aws/lib/codestarnotifications-notification-rule'; +import { DataAwsCallerIdentity } from '@cdktf/provider-aws/lib/data-aws-caller-identity'; +import { DataAwsIamPolicyDocument } from '@cdktf/provider-aws/lib/data-aws-iam-policy-document'; +import { DataAwsRegion } from '@cdktf/provider-aws/lib/data-aws-region'; +import { IamRole } from '@cdktf/provider-aws/lib/iam-role'; +import { IamRolePolicyAttachment } from '@cdktf/provider-aws/lib/iam-role-policy-attachment'; +import { TerraformMetaArguments, TerraformResource } from 'cdktf'; +import { Construct } from 'constructs'; + +export interface ApplicationECSAlbCodeDeployProps + extends TerraformMetaArguments { + prefix: string; + clusterName: string; + serviceName: string; + listenerArn: string; + snsNotificationTopicArn?: string; + targetGroupNames: string[]; + tags?: { [key: string]: string }; + dependsOn?: TerraformResource[]; + successTerminationWaitTimeInMinutes?: number; + notifications?: { + notifyOnStarted?: boolean; + notifyOnSucceeded?: boolean; + notifyOnFailed?: boolean; + }; +} + +interface CodeDeployResponse { + codeDeployApp: CodedeployApp; + ecsCodeDeployRole: IamRole; +} + +/** + * Represents a ecs Codeploy App that uses an ALB + */ +export class ApplicationECSAlbCodeDeploy extends Construct { + private readonly config: ApplicationECSAlbCodeDeployProps; + + public readonly codeDeployApp: CodedeployApp; + public readonly codeDeployDeploymentGroup: CodedeployDeploymentGroup; + + constructor( + scope: Construct, + name: string, + config: ApplicationECSAlbCodeDeployProps, + ) { + super(scope, name); + + this.config = config; + + const { codeDeployApp, ecsCodeDeployRole } = this.setupCodeDeployApp(); + this.codeDeployApp = codeDeployApp; + + this.codeDeployDeploymentGroup = new CodedeployDeploymentGroup( + this, + `ecs_codedeploy_deployment_group`, + { + dependsOn: config.dependsOn, + appName: codeDeployApp.name, + deploymentConfigName: 'CodeDeployDefault.ECSAllAtOnce', + deploymentGroupName: `${this.config.prefix}-ECS`, + serviceRoleArn: ecsCodeDeployRole.arn, + autoRollbackConfiguration: { + enabled: true, + events: ['DEPLOYMENT_FAILURE'], + }, + blueGreenDeploymentConfig: { + deploymentReadyOption: { + actionOnTimeout: 'CONTINUE_DEPLOYMENT', + }, + terminateBlueInstancesOnDeploymentSuccess: { + action: 'TERMINATE', + terminationWaitTimeInMinutes: + (this.config.successTerminationWaitTimeInMinutes ??= 5), + }, + }, + deploymentStyle: { + deploymentOption: 'WITH_TRAFFIC_CONTROL', + deploymentType: 'BLUE_GREEN', + }, + ecsService: { + clusterName: this.config.clusterName, + serviceName: this.config.serviceName, + }, + loadBalancerInfo: { + targetGroupPairInfo: { + prodTrafficRoute: { listenerArns: [this.config.listenerArn] }, + targetGroup: this.config.targetGroupNames.map((name) => { + return { name }; + }), + }, + }, + tags: this.config.tags, + provider: this.config.provider, + }, + ); + } + + /** + * Set configuration for code deploy notifications + * @param notifyOnStarted + * @param notifyOnSucceeded + * @param notifyOnFailed + * @returns An array of EventTypeIds + */ + + private getEventTypeIds( + notifyOnStarted = true, + notifyOnSucceeded = true, + notifyOnFailed = true, + ): string[] { + const eventTypeIds: string[] = []; + + if (notifyOnFailed) { + eventTypeIds.push('codedeploy-application-deployment-failed'); + } + + if (notifyOnSucceeded) { + eventTypeIds.push('codedeploy-application-deployment-succeeded'); + } + + if (notifyOnStarted) { + eventTypeIds.push('codedeploy-application-deployment-started'); + } + + return eventTypeIds; + } + + /** + * Setup the codedeploy app, permissions, and notifications + * @private + */ + private setupCodeDeployApp(): CodeDeployResponse { + const ecsCodeDeployRole = new IamRole(this, 'ecs_code_deploy_role', { + name: `${this.config.prefix}-ECSCodeDeployRole`, + assumeRolePolicy: new DataAwsIamPolicyDocument( + this, + `codedeploy_assume_role`, + { + statement: [ + { + effect: 'Allow', + actions: ['sts:AssumeRole'], + principals: [ + { + identifiers: ['codedeploy.amazonaws.com'], + type: 'Service', + }, + ], + }, + ], + }, + ).json, + tags: this.config.tags, + provider: this.config.provider, + }); + + new IamRolePolicyAttachment(this, 'ecs_codedeploy_role_attachment', { + policyArn: 'arn:aws:iam::aws:policy/AWSCodeDeployRoleForECS', + role: ecsCodeDeployRole.name, + dependsOn: [ecsCodeDeployRole], + provider: this.config.provider, + }); + + const codeDeployApp = new CodedeployApp(this, 'ecs_code_deploy', { + computePlatform: 'ECS', + name: `${this.config.prefix}-ECS`, + tags: this.config.tags, + provider: this.config.provider, + }); + + if (this.config.snsNotificationTopicArn) { + const region = new DataAwsRegion(this, 'current_region', { + provider: this.config.provider, + }); + const account = new DataAwsCallerIdentity(this, 'current_account', { + provider: this.config.provider, + }); + new CodestarnotificationsNotificationRule( + this, + `ecs_codedeploy_notifications`, + { + detailType: 'BASIC', + eventTypeIds: this.getEventTypeIds( + this.config.notifications?.notifyOnStarted, + this.config.notifications?.notifyOnSucceeded, + this.config.notifications?.notifyOnFailed, + ), + name: codeDeployApp.name, + resource: `arn:aws:codedeploy:${region.name}:${account.accountId}:application:${codeDeployApp.name}`, + target: [ + { + address: this.config.snsNotificationTopicArn, + }, + ], + tags: this.config.tags, + provider: this.config.provider, + }, + ); + } + + return { codeDeployApp, ecsCodeDeployRole }; + } +} diff --git a/packages/terraform-modules/src/base/ApplicationECSCluster.spec.ts b/packages/terraform-modules/src/base/ApplicationECSCluster.spec.ts new file mode 100644 index 00000000..3465ebcc --- /dev/null +++ b/packages/terraform-modules/src/base/ApplicationECSCluster.spec.ts @@ -0,0 +1,26 @@ +import { Testing } from 'cdktf'; +import { ApplicationECSCluster } from './ApplicationECSCluster'; + +describe('ApplicationECSCluster', () => { + it('renders an ECS cluster without tags', () => { + const synthed = Testing.synthScope((stack) => { + new ApplicationECSCluster(stack, 'testECSCluster', { + prefix: 'bowling-', + }); + }); + expect(synthed).toMatchSnapshot(); + }); + + it('renders an ECS cluster with tags', () => { + const synthed = Testing.synthScope((stack) => { + new ApplicationECSCluster(stack, 'testECSCluster', { + prefix: 'bowling-', + tags: { + name: 'maude', + description: 'artist', + }, + }); + }); + expect(synthed).toMatchSnapshot(); + }); +}); diff --git a/packages/terraform-modules/src/base/ApplicationECSCluster.ts b/packages/terraform-modules/src/base/ApplicationECSCluster.ts new file mode 100644 index 00000000..15f7baaa --- /dev/null +++ b/packages/terraform-modules/src/base/ApplicationECSCluster.ts @@ -0,0 +1,35 @@ +import { EcsCluster } from '@cdktf/provider-aws/lib/ecs-cluster'; +import { TerraformMetaArguments } from 'cdktf'; +import { Construct } from 'constructs'; + +export interface ApplicationECSClusterProps extends TerraformMetaArguments { + prefix: string; + tags?: { [key: string]: string }; +} + +/** + * Generates an Application Certificate given a domain name and zoneId + */ +export class ApplicationECSCluster extends Construct { + public readonly cluster: EcsCluster; + + constructor( + scope: Construct, + name: string, + config: ApplicationECSClusterProps, + ) { + super(scope, name); + + this.cluster = new EcsCluster(this, `ecs_cluster`, { + tags: config.tags, + name: config.prefix, + setting: [ + { + name: 'containerInsights', + value: 'enabled', + }, + ], + provider: config.provider, + }); + } +} diff --git a/packages/terraform-modules/src/base/ApplicationECSContainerDefinition.spec.ts b/packages/terraform-modules/src/base/ApplicationECSContainerDefinition.spec.ts new file mode 100644 index 00000000..cdcd8bdd --- /dev/null +++ b/packages/terraform-modules/src/base/ApplicationECSContainerDefinition.spec.ts @@ -0,0 +1,178 @@ +import { + ApplicationECSContainerDefinitionProps, + buildDefinitionJSON, +} from './ApplicationECSContainerDefinition'; + +describe('ApplicationECSContainerDefinition', () => { + describe('buildDefinitionJSON', () => { + let config: ApplicationECSContainerDefinitionProps; + + beforeEach(() => { + config = { + containerImage: 'testImage', + logGroup: 'bowlingGroup', + logMultilinePattern: '^\\S.+', + logDatetimeFormat: '[%b %d, %Y %H:%M:%S]', + portMappings: [ + { + hostPort: 3000, + containerPort: 4000, + }, + ], + ulimits: [ + { + name: 'nproc', + softLimit: 2048, + hardLimit: 2048, + }, + { + name: 'nofile', + softLimit: 65535, + hardLimit: 65535, + }, + ], + name: 'lebowski', + repositoryCredentialsParam: 'someArn', + }; + }); + + it('builds JSON without env vars', () => { + const result = buildDefinitionJSON(config); + + expect(result).toContain('awslogs-group":"bowlingGroup"'); + expect(result).toContain('"awslogs-multiline-pattern":"^\\\\S.+"'); + expect(result).toContain( + '"awslogs-datetime-format":"[%b %d, %Y %H:%M:%S]"', + ); + expect(result).toContain('"hostPort":3000'); + expect(result).toContain('"containerPort":4000'); + expect(result).toContain('"image":"testImage"'); + expect(result).toContain('"name":"lebowski"'); + expect(result).toContain('"credentialsParameter":"someArn"'); + expect(result).toContain('"environment":[]'); + expect(result).toContain('"secrets":null'); + expect(result).toContain('"softLimit":2048'); + expect(result).toContain('"hardLimit":65535'); + expect(result).not.toContain('"command":'); + }); + + it('builds JSON with env vars', () => { + config.envVars = [ + { + name: 'dude', + value: 'abides', + }, + { + name: 'letsgo', + value: 'bowling', + }, + ]; + + const result = buildDefinitionJSON(config); + + expect(result).toContain( + `"environment":${JSON.stringify(config.envVars)}`, + ); + }); + + it('builds JSON with secret env vars', () => { + config.secretEnvVars = [ + { + name: 'dude', + valueFrom: 'abides', + }, + { + name: 'letsgo', + valueFrom: 'bowling', + }, + ]; + + const result = buildDefinitionJSON(config); + + expect(result).toContain( + `"secrets":${JSON.stringify(config.secretEnvVars)}`, + ); + }); + + it('builds JSON with a command', () => { + config.command = ['go to in-n-out', 'go bowling']; + + const result = buildDefinitionJSON(config); + + expect(result).toContain(`"command":["go to in-n-out","go bowling"]`); + }); + + it('builds JSON with a healthcheck', () => { + config.healthCheck = { + command: [ + 'CMD-SHELL', + 'curl -f "http://127.0.0.1:8000/pulse" || exit 1', + ], + interval: 30, + retries: 2, + startPeriod: 0, + timeout: 5, + }; + + const result = buildDefinitionJSON(config); + + expect(result).toContain( + `"healthCheck":{` + + `"command":["CMD-SHELL","curl -f \\"http://127.0.0.1:8000/pulse\\" || exit 1"],` + + `"interval":30,` + + `"retries":2,` + + `"startPeriod":0,` + + `"timeout":5}`, + ); + }); + + it('builds JSON without repository credentials', () => { + config.repositoryCredentialsParam = undefined; + + const result = buildDefinitionJSON(config); + + expect(result).toContain(`"repositoryCredentials":null,`); + }); + + it('builds JSON with mountPoints', () => { + config.mountPoints = [ + { + containerPath: '/"-".txt', + readOnly: true, + sourceVolume: '/[{}].txt', + }, + ]; + + const result = buildDefinitionJSON(config); + + expect(result).toContain( + `"mountPoints":[{` + + `"containerPath":"/\\"-\\".txt",` + + `"readOnly":true,` + + `"sourceVolume":"/[{}].txt"}`, + ); + }); + + it('passes entryPoint', () => { + config.entryPoint = ['/bin/bash']; + + const result = buildDefinitionJSON(config); + + expect(result).toContain(`"entryPoint":["/bin/bash"]`); + }); + + it('passes essential', () => { + config.essential = false; + + const result = buildDefinitionJSON(config); + + expect(result).toContain(`"essential":false`); + }); + + it('essential defaults to true', () => { + const result = buildDefinitionJSON(config); + + expect(result).toContain(`"essential":true`); + }); + }); +}); diff --git a/packages/terraform-modules/src/base/ApplicationECSContainerDefinition.ts b/packages/terraform-modules/src/base/ApplicationECSContainerDefinition.ts new file mode 100644 index 00000000..5195e897 --- /dev/null +++ b/packages/terraform-modules/src/base/ApplicationECSContainerDefinition.ts @@ -0,0 +1,137 @@ +interface EnvironmentVariable { + name: string; + value: string; +} + +interface SecretEnvironmentVariable { + name: string; + valueFrom: string; +} + +interface HealthcheckVariable { + command: string[]; + interval: number; + retries: number; + startPeriod: number; + timeout: number; +} + +interface PortMapping { + containerPort: number; + hostPort: number; + protocol?: string; +} + +interface MountPoint { + containerPath: string; + readOnly?: boolean; + sourceVolume: string; +} + +interface DependsOn { + containerName: string; + condition: 'START' | 'COMPLETE' | 'SUCCESS' | 'HEALTHY'; +} + +interface Ulimit { + name: string; + softLimit: number; + hardLimit: number; +} + +export interface ApplicationECSContainerDefinitionProps { + containerImage?: string; + logDatetimeFormat?: string; + logGroup?: string; + logGroupRegion?: string; + logMultilinePattern?: string; + logStreamPrefix?: string; + portMappings?: PortMapping[]; + envVars?: EnvironmentVariable[]; + secretEnvVars?: SecretEnvironmentVariable[]; + command?: string[]; + name: string; + repositoryCredentialsParam?: string; + memoryReservation?: number; + cpu?: number; + healthCheck?: HealthcheckVariable; + mountPoints?: MountPoint[]; + dependsOn?: DependsOn[]; + entryPoint?: string[]; + essential?: boolean; + ulimits?: Ulimit[]; +} + +export function buildDefinitionJSON( + config: ApplicationECSContainerDefinitionProps, +): string { + // logGroup is optional in PocketALBApplicationProps, and provides a default log group if unset. + if (!config.logGroup) { + throw new Error('logGroup is required in buildDefinitionJSON'); + } + + const containerDefinition = { + dnsSearchDomains: null, + environmentFiles: null, + logConfiguration: { + logDriver: 'awslogs', + secretOptions: [], + options: { + 'awslogs-group': config.logGroup, + 'awslogs-region': config.logGroupRegion ?? 'us-east-1', + 'awslogs-stream-prefix': config.logStreamPrefix ?? 'ecs', + // datetime takes precedence if datetime and multiline defined + ...(config.logDatetimeFormat && { + 'awslogs-datetime-format': config.logDatetimeFormat, + }), + // regex parsing - may have negative impact on logging performance + ...(config.logMultilinePattern && { + 'awslogs-multiline-pattern': config.logMultilinePattern, + }), + }, + }, + entryPoint: config.entryPoint ?? null, + portMappings: config.portMappings ?? [], + linuxParameters: null, + cpu: config.cpu ?? 0, + environment: config.envVars ?? [], + resourceRequirements: null, + ulimits: config.ulimits ?? null, + repositoryCredentials: config.repositoryCredentialsParam + ? { credentialsParameter: config.repositoryCredentialsParam } + : null, + dnsServers: null, + mountPoints: config.mountPoints ?? [], + workingDirectory: null, + secrets: config.secretEnvVars ?? null, // env vars default is [], whereas secrets default is null. makes sense. + dockerSecurityOptions: null, + memory: null, + memoryReservation: config.memoryReservation ?? null, + volumesFrom: [], + stopTimeout: null, + image: config.containerImage, + startTimeout: null, + firelensConfiguration: null, + dependsOn: config.dependsOn ?? null, + disableNetworking: null, + interactive: null, + healthCheck: config.healthCheck ?? null, + essential: config.essential ?? true, + links: null, + hostname: null, + extraHosts: null, + pseudoTerminal: null, + user: null, + readonlyRootFilesystem: false, + dockerLabels: null, + systemControls: null, + privileged: null, + name: config.name, + }; + + if (config.command) { + containerDefinition['command'] = config.command; + } + + return JSON.stringify(containerDefinition); +} diff --git a/packages/terraform-modules/src/base/ApplicationECSIAM.spec.ts b/packages/terraform-modules/src/base/ApplicationECSIAM.spec.ts new file mode 100644 index 00000000..f40b457e --- /dev/null +++ b/packages/terraform-modules/src/base/ApplicationECSIAM.spec.ts @@ -0,0 +1,30 @@ +import { Testing } from 'cdktf'; +import { ApplicationECSIAM, ApplicationECSIAMProps } from './ApplicationECSIAM'; + +const BASE_CONFIG: ApplicationECSIAMProps = { + prefix: 'abides-dev', + taskExecutionDefaultAttachmentArn: 'someArn', + taskExecutionRolePolicyStatements: [], + taskRolePolicyStatements: [], +}; + +describe('ApplicationECSIAM', () => { + it('renders ECS IAM with minimal config', () => { + const synthed = Testing.synthScope((stack) => { + new ApplicationECSIAM(stack, 'testECSService', BASE_CONFIG); + }); + expect(synthed).toMatchSnapshot(); + }); + + it('renders ECS IAM with tags', () => { + BASE_CONFIG.tags = { + letsgo: 'bowling', + donnie: 'throwinrockstonight', + }; + + const synthed = Testing.synthScope((stack) => { + new ApplicationECSIAM(stack, 'testECSService', BASE_CONFIG); + }); + expect(synthed).toMatchSnapshot(); + }); +}); diff --git a/packages/terraform-modules/src/base/ApplicationECSIAM.ts b/packages/terraform-modules/src/base/ApplicationECSIAM.ts new file mode 100644 index 00000000..99c36533 --- /dev/null +++ b/packages/terraform-modules/src/base/ApplicationECSIAM.ts @@ -0,0 +1,138 @@ +import { + DataAwsIamPolicyDocumentStatement, + DataAwsIamPolicyDocument, +} from '@cdktf/provider-aws/lib/data-aws-iam-policy-document'; +import { IamPolicy } from '@cdktf/provider-aws/lib/iam-policy'; +import { IamRole } from '@cdktf/provider-aws/lib/iam-role'; +import { IamRolePolicyAttachment } from '@cdktf/provider-aws/lib/iam-role-policy-attachment'; +import { TerraformMetaArguments } from 'cdktf'; +import { Construct } from 'constructs'; + +export interface ApplicationECSIAMProps extends TerraformMetaArguments { + prefix: string; + taskExecutionRolePolicyStatements: DataAwsIamPolicyDocumentStatement[]; + taskRolePolicyStatements: DataAwsIamPolicyDocumentStatement[]; + taskExecutionDefaultAttachmentArn?: string; + tags?: { [key: string]: string }; +} + +export class ApplicationECSIAM extends Construct { + public readonly taskExecutionRoleArn; + public readonly taskRoleArn; + public readonly taskRole: IamRole; + + constructor(scope: Construct, name: string, config: ApplicationECSIAMProps) { + super(scope, name); + + // does anything here need to be in config? + const dataEcsTaskAssume = new DataAwsIamPolicyDocument( + this, + 'ecs-task-assume', + { + version: '2012-10-17', + statement: [ + { + effect: 'Allow', + actions: ['sts:AssumeRole'], + principals: [ + { + identifiers: ['ecs-tasks.amazonaws.com'], + type: 'Service', + }, + ], + }, + ], + provider: config.provider, + }, + ); + + const ecsTaskExecutionRole = new IamRole(this, 'ecs-execution-role', { + assumeRolePolicy: dataEcsTaskAssume.json, + name: `${config.prefix}-TaskExecutionRole`, + tags: config.tags, + provider: config.provider, + }); + + if (config.taskExecutionDefaultAttachmentArn) { + new IamRolePolicyAttachment( + this, + 'ecs-task-execution-default-attachment', + { + policyArn: config.taskExecutionDefaultAttachmentArn, + role: ecsTaskExecutionRole.id, + provider: config.provider, + }, + ); + } + + if (config.taskExecutionRolePolicyStatements.length > 0) { + const dataEcsTaskExecutionRolePolicy = new DataAwsIamPolicyDocument( + this, + 'data-ecs-task-execution-role-policy', + { + version: '2012-10-17', + statement: config.taskExecutionRolePolicyStatements, + provider: config.provider, + }, + ); + + const ecsTaskExecutionRolePolicy = new IamPolicy( + this, + 'ecs-task-execution-role-policy', + { + name: `${config.prefix}-TaskExecutionRolePolicy`, + policy: dataEcsTaskExecutionRolePolicy.json, + provider: config.provider, + tags: config.tags, + }, + ); + + new IamRolePolicyAttachment( + this, + 'ecs-task-execution-custom-attachment', + { + policyArn: ecsTaskExecutionRolePolicy.arn, + role: ecsTaskExecutionRole.id, + provider: config.provider, + }, + ); + } + + const ecsTaskRole = new IamRole(this, 'ecs-task-role', { + assumeRolePolicy: dataEcsTaskAssume.json, + name: `${config.prefix}-TaskRole`, + tags: config.tags, + provider: config.provider, + }); + + if (config.taskRolePolicyStatements.length > 0) { + const dataEcsTaskRolePolicy = new DataAwsIamPolicyDocument( + this, + 'data-ecs-task-role-policy', + { + version: '2012-10-17', + statement: config.taskRolePolicyStatements, + provider: config.provider, + }, + ); + + const ecsTaskRolePolicy = new IamPolicy(this, 'ecs-task-role-policy', { + name: `${config.prefix}-TaskRolePolicy`, + policy: dataEcsTaskRolePolicy.json, + provider: config.provider, + tags: config.tags, + }); + + new IamRolePolicyAttachment(this, 'ecs-task-custom-attachment', { + policyArn: ecsTaskRolePolicy.arn, + role: ecsTaskRole.id, + provider: config.provider, + }); + } + + // make arns available to other modules + this.taskExecutionRoleArn = ecsTaskExecutionRole.arn; + this.taskRoleArn = ecsTaskRole.arn; + this.taskRole = ecsTaskRole; + } +} diff --git a/packages/terraform-modules/src/base/ApplicationECSService.spec.ts b/packages/terraform-modules/src/base/ApplicationECSService.spec.ts new file mode 100644 index 00000000..d2386e0f --- /dev/null +++ b/packages/terraform-modules/src/base/ApplicationECSService.spec.ts @@ -0,0 +1,323 @@ +import { Testing } from 'cdktf'; +import { + ApplicationECSService, + ApplicationECSServiceProps, +} from './ApplicationECSService'; + +let BASE_CONFIG: ApplicationECSServiceProps; + +const testAlbConfig: ApplicationECSServiceProps['albConfig'] = { + healthCheckPath: '/health', + listenerArn: 'listen-to-me', + containerPort: 3000, + containerName: 'runme', + albSecurityGroupId: 'strike', +}; + +describe('ApplicationECSService', () => { + beforeEach(() => { + BASE_CONFIG = { + ecsClusterName: 'cluster-name', + shortName: 'short', + useCodeDeploy: false, + prefix: 'abides-dev', + ecsClusterArn: 'gorp', + vpcId: 'myhouse', + containerConfigs: [], + privateSubnetIds: ['1.1.1.1', '2.2.2.2'], + ecsIamConfig: { + prefix: 'abides-', + taskExecutionDefaultAttachmentArn: 'someArn', + taskExecutionRolePolicyStatements: [], + taskRolePolicyStatements: [], + }, + }; + }); + + it('renders an ECS service with minimal config', () => { + const synthed = Testing.synthScope((stack) => { + new ApplicationECSService(stack, 'testECSService', BASE_CONFIG); + }); + expect(synthed).toMatchSnapshot(); + }); + + it('renders an ECS service with full container definition props', () => { + BASE_CONFIG.launchType = 'ROCKET'; + BASE_CONFIG.deploymentMaximumPercent = 400; + BASE_CONFIG.deploymentMinimumHealthyPercent = 80; + BASE_CONFIG.desiredCount = 4; + BASE_CONFIG.lifecycleIgnoreChanges = ['bowling', 'donnie', 'autobahn']; + BASE_CONFIG.containerConfigs = [ + { + portMappings: [ + { + containerPort: 3002, + hostPort: 3001, + }, + ], + logGroup: 'test/log/group', + containerImage: 'beverage-here/0.1', + name: 'lebowski', + repositoryCredentialsParam: 'someArn', + envVars: [ + { + name: 'rug', + value: 'tiedtheroomtogether', + }, + ], + secretEnvVars: [ + { + name: 'donnie', + valueFrom: 'throwinrockstonight', + }, + ], + }, + ]; + const synthed = Testing.synthScope((stack) => { + new ApplicationECSService(stack, 'testECSService', BASE_CONFIG); + }); + expect(synthed).toMatchSnapshot(); + }); + + it('renders an ECS service without a log group container definition props', () => { + BASE_CONFIG.launchType = 'ROCKET'; + BASE_CONFIG.deploymentMaximumPercent = 400; + BASE_CONFIG.deploymentMinimumHealthyPercent = 80; + BASE_CONFIG.desiredCount = 4; + BASE_CONFIG.lifecycleIgnoreChanges = ['bowling', 'donnie', 'autobahn']; + BASE_CONFIG.containerConfigs = [ + { + portMappings: [ + { + containerPort: 3002, + hostPort: 3001, + }, + ], + containerImage: 'beverage-here/0.1', + name: 'lebowski', + repositoryCredentialsParam: 'someArn', + envVars: [ + { + name: 'rug', + value: 'tiedtheroomtogether', + }, + ], + secretEnvVars: [ + { + name: 'donnie', + valueFrom: 'throwinrockstonight', + }, + ], + }, + ]; + + const synthed = Testing.synthScope((stack) => { + new ApplicationECSService(stack, 'testECSService', BASE_CONFIG); + }); + expect(synthed).toMatchSnapshot(); + }); + + it('renders an ECS service without an image container definition props', () => { + BASE_CONFIG.launchType = 'ROCKET'; + BASE_CONFIG.deploymentMaximumPercent = 400; + BASE_CONFIG.deploymentMinimumHealthyPercent = 80; + BASE_CONFIG.desiredCount = 4; + BASE_CONFIG.lifecycleIgnoreChanges = ['bowling', 'donnie', 'autobahn']; + BASE_CONFIG.containerConfigs = [ + { + portMappings: [ + { + containerPort: 3002, + hostPort: 3001, + }, + ], + logGroup: 'test/log/group', + name: 'lebowski', + repositoryCredentialsParam: 'someArn', + envVars: [ + { + name: 'rug', + value: 'tiedtheroomtogether', + }, + ], + secretEnvVars: [ + { + name: 'donnie', + valueFrom: 'throwinrockstonight', + }, + ], + }, + ]; + + const synthed = Testing.synthScope((stack) => { + new ApplicationECSService(stack, 'testECSService', BASE_CONFIG); + }); + expect(synthed).toMatchSnapshot(); + }); + + it('renders an ECS service with full container definition props and ALB security group config', () => { + BASE_CONFIG.containerConfigs = [ + { + portMappings: [ + { + containerPort: 3002, + hostPort: 3001, + }, + ], + logGroup: 'test/log/group', + containerImage: 'beverage-here/0.1', + name: 'lebowski', + repositoryCredentialsParam: 'someArn', + envVars: [ + { + name: 'rug', + value: 'tiedtheroomtogether', + }, + ], + secretEnvVars: [ + { + name: 'donnie', + valueFrom: 'throwinrockstonight', + }, + ], + }, + ]; + + BASE_CONFIG.albConfig = testAlbConfig; + + const synthed = Testing.synthScope((stack) => { + new ApplicationECSService(stack, 'testECSService', BASE_CONFIG); + }); + expect(synthed).toMatchSnapshot(); + }); + + it('renders an ECS service with mountPoints deduplicated in the task definition', () => { + BASE_CONFIG.launchType = 'ROCKET'; + BASE_CONFIG.containerConfigs = [ + { + name: 'container1', + }, + { + name: 'container2', + mountPoints: [ + { + sourceVolume: 'src1', + containerPath: '/src1', + }, + { + sourceVolume: 'src2', + containerPath: '/src2', + }, + ], + }, + { + name: 'container3', + mountPoints: [ + { + sourceVolume: 'src1', + containerPath: '/src1', + readOnly: true, + }, + { + sourceVolume: 'src3', + containerPath: '/src3', + }, + ], + }, + ]; + + const synthed = Testing.synthScope((stack) => { + new ApplicationECSService(stack, 'testECSService', BASE_CONFIG); + }); + expect(synthed).toMatchSnapshot(); + }); + + it('renders an ECS service with code deploy', () => { + const synthed = Testing.synthScope((stack) => { + new ApplicationECSService(stack, 'testECSService', { + ...BASE_CONFIG, + ...testAlbConfig, + useCodeDeploy: true, + }); + }); + expect(synthed).toMatchSnapshot(); + }); + + it('renders an ECS service with code deploy notifications set for failed only', () => { + const synthed = Testing.synthScope((stack) => { + new ApplicationECSService(stack, 'testECSService', { + ...BASE_CONFIG, + ...testAlbConfig, + useCodeDeploy: true, + codeDeployNotifications: { + notifyOnFailed: true, + notifyOnStarted: false, + notifyOnSucceeded: false, + }, + }); + }); + expect(synthed).toMatchSnapshot(); + }); + + it('renders an ECS service with code deploy and excludes the code deployment command resource when useCodePipeline is true', () => { + const synthed = Testing.synthScope((stack) => { + new ApplicationECSService(stack, 'testECSService', { + ...BASE_CONFIG, + ...testAlbConfig, + useCodeDeploy: true, + useCodePipeline: true, + }); + }); + expect(synthed).toMatchSnapshot(); + }); + + it('exposes ECR repos and task definition as public fields', () => { + BASE_CONFIG.containerConfigs = [ + { + portMappings: [ + { + containerPort: 3002, + hostPort: 3001, + }, + ], + name: 'lebowski', + }, + ]; + + Testing.synthScope((stack) => { + const applicationECSService = new ApplicationECSService( + stack, + 'testECSService', + BASE_CONFIG, + ); + + expect(applicationECSService.ecrRepos.length).toEqual(1); + expect( + applicationECSService.taskDefinition.terraformResourceType, + ).toEqual('aws_ecs_task_definition'); + }); + }); + + it('attaches persistent (efs) storage to a ECS task', () => { + BASE_CONFIG.containerConfigs = [ + { + mountPoints: [ + { + containerPath: '/someMountPoint', + sourceVolume: 'sourceVolume', + }, + ], + name: 'lebowski', + }, + ]; + BASE_CONFIG.efsConfig = { + efs: { arn: 'fakeArn', id: 'someId' }, + volumeName: 'sourceVolume', + }; + + const synthed = Testing.synthScope((stack) => { + new ApplicationECSService(stack, 'testECSService', BASE_CONFIG); + }); + expect(synthed).toMatchSnapshot(); + }); +}); diff --git a/packages/terraform-modules/src/base/ApplicationECSService.ts b/packages/terraform-modules/src/base/ApplicationECSService.ts new file mode 100644 index 00000000..17b397da --- /dev/null +++ b/packages/terraform-modules/src/base/ApplicationECSService.ts @@ -0,0 +1,611 @@ +import { Resource } from '@cdktf/provider-null/lib/resource'; +import { Sleep } from '@cdktf/provider-time/lib/sleep'; +import { Construct } from 'constructs'; +import { ApplicationECR, ECRProps } from './ApplicationECR'; +import { ApplicationECSIAM, ApplicationECSIAMProps } from './ApplicationECSIAM'; +import { + ApplicationECSContainerDefinitionProps, + buildDefinitionJSON, +} from './ApplicationECSContainerDefinition'; +import { ApplicationTargetGroup } from './ApplicationTargetGroup'; +import { ApplicationECSAlbCodeDeploy } from './ApplicationECSAlbCodeDeploy'; +import { + TerraformResource, + TerraformIterator, + TerraformMetaArguments, + Fn, +} from 'cdktf'; +import { truncateString } from '../utilities'; +import { File } from '@cdktf/provider-local/lib/file'; +import { AlbListenerRule } from '@cdktf/provider-aws/lib/alb-listener-rule'; +import { CloudwatchLogGroup } from '@cdktf/provider-aws/lib/cloudwatch-log-group'; +import { EcrRepository } from '@cdktf/provider-aws/lib/ecr-repository'; +import { + EcsService, + EcsServiceNetworkConfiguration, + EcsServiceLoadBalancer, +} from '@cdktf/provider-aws/lib/ecs-service'; +import { + EcsTaskDefinition, + EcsTaskDefinitionVolume, +} from '@cdktf/provider-aws/lib/ecs-task-definition'; +import { EfsFileSystemPolicy } from '@cdktf/provider-aws/lib/efs-file-system-policy'; +import { EfsMountTarget } from '@cdktf/provider-aws/lib/efs-mount-target'; +import { + SecurityGroup, + SecurityGroupIngress, + SecurityGroupEgress, +} from '@cdktf/provider-aws/lib/security-group'; + +const defaultRegion = 'us-east-1'; + +export interface ApplicationECSServiceProps extends TerraformMetaArguments { + prefix: string; + region?: string; + shortName: string; + tags?: { [key: string]: string }; + ecsClusterArn: string; + ecsClusterName: string; + vpcId: string; + albConfig?: { + containerPort: number; + containerName: string; + healthCheckPath: string; + albSecurityGroupId: string; + listenerArn: string; + }; + containerConfigs: ApplicationECSContainerDefinitionProps[]; + privateSubnetIds: string[]; + cpu?: number; // defaults to 512 + memory?: number; // defaults to 2048 + launchType?: string; // defaults to 'FARGATE' + deploymentMinimumHealthyPercent?: number; // defaults to 100 + deploymentMaximumPercent?: number; // defaults to 200 + desiredCount?: number; // defaults to 2 + lifecycleIgnoreChanges?: string[]; // defaults to ['task_definition', 'desired_count', 'load_balancer'] + ecsIamConfig: ApplicationECSIAMProps; + useCodeDeploy: boolean; //defaults to true + useTerraformBasedCodeDeploy?: boolean; //defaults to true + useCodePipeline?: boolean; + successTerminationWaitTimeInMinutes?: number; + codeDeployNotifications?: { + notifyOnStarted?: boolean; //defaults to true + notifyOnSucceeded?: boolean; //defaults to true + notifyOnFailed?: boolean; //defaults to true + }; + efsConfig?: { + efs: EFSProps; + volumeName: string; + }; + + codeDeploySnsNotificationTopicArn?: string; +} + +export interface EFSProps { + id: string; + arn: string; +} +interface ECSTaskDefinitionResponse { + taskDef: EcsTaskDefinition; + ecrRepos: EcrRepository[]; +} + +/** + * Generates an Application Certificate given a domain name and zoneId + */ +export class ApplicationECSService extends Construct { + public readonly service: EcsService; + public readonly ecsSecurityGroup: SecurityGroup; + public readonly mainTargetGroup?: ApplicationTargetGroup; + public readonly codeDeployApp?: ApplicationECSAlbCodeDeploy; + public readonly ecrRepos: EcrRepository[]; + public readonly taskDefinition: EcsTaskDefinition; + public ecsIam: ApplicationECSIAM; + private readonly config: ApplicationECSServiceProps; + + constructor( + scope: Construct, + name: string, + config: ApplicationECSServiceProps, + ) { + super(scope, name); + + // populate defaults for some values if not provided + this.config = ApplicationECSService.hydrateConfig(config); + + // set default region + this.config.region = this.config.region ?? defaultRegion; + + this.ecsSecurityGroup = this.setupECSSecurityGroups(); + const { taskDef, ecrRepos } = this.setupECSTaskDefinition(); + this.taskDefinition = taskDef; + this.ecrRepos = ecrRepos; + (''); + //Setup an array of resources that the ecs service will need to depend on + const ecsServiceDependsOn: TerraformResource[] = [...ecrRepos]; + + const ecsNetworkConfig: EcsServiceNetworkConfiguration = { + securityGroups: [this.ecsSecurityGroup.id], + subnets: config.privateSubnetIds, + }; + + const ecsLoadBalancerConfig: EcsServiceLoadBalancer[] = []; + + const targetGroupNames: string[] = []; + + // If we have a alb configuration lets add it. + if (config.albConfig) { + this.mainTargetGroup = this.createTargetGroup('blue'); + ecsServiceDependsOn.push(this.mainTargetGroup.targetGroup); + // Now that we have our service created, we append the alb listener rule to our HTTPS listener. + const listenerRule = new AlbListenerRule(this, 'listener_rule', { + listenerArn: this.config.albConfig.listenerArn, + priority: 1, + condition: [ + { + pathPattern: { values: ['*'] }, + }, + ], + action: [ + { + type: 'forward', + targetGroupArn: this.mainTargetGroup.targetGroup.arn, + }, + ], + lifecycle: { + ignoreChanges: ['action'], + }, + provider: config.provider, + tags: this.config.tags, + }); + ecsServiceDependsOn.push(listenerRule); + targetGroupNames.push(this.mainTargetGroup.targetGroup.name); + ecsLoadBalancerConfig.push({ + containerName: config.albConfig.containerName, + containerPort: config.albConfig.containerPort, + targetGroupArn: this.mainTargetGroup.targetGroup.arn, + }); + } + + //create ecs service + this.service = new EcsService(this, 'ecs-service', { + name: `${this.config.prefix}`, + taskDefinition: taskDef.arn, + deploymentController: this.config.useCodeDeploy + ? { type: 'CODE_DEPLOY' } + : { type: 'ECS' }, + launchType: this.config.launchType, + deploymentMinimumHealthyPercent: + this.config.deploymentMinimumHealthyPercent, + deploymentMaximumPercent: this.config.deploymentMaximumPercent, + desiredCount: this.config.desiredCount, + cluster: this.config.ecsClusterArn, + loadBalancer: ecsLoadBalancerConfig, + networkConfiguration: ecsNetworkConfig, + propagateTags: 'SERVICE', + lifecycle: { + ignoreChanges: this.config.lifecycleIgnoreChanges, + }, + tags: this.config.tags, + dependsOn: ecsServiceDependsOn, + provider: this.config.provider, + }); + + if (this.config.useCodeDeploy && this.config.albConfig) { + //Setup a second target group + const greenTargetGroup = this.createTargetGroup('green'); + targetGroupNames.push(greenTargetGroup.targetGroup.name); + //Setup codedeploy + const codeDeployApp = (this.codeDeployApp = + new ApplicationECSAlbCodeDeploy(this, 'ecs_codedeploy', { + prefix: this.config.prefix, + serviceName: this.service.name, + clusterName: this.config.ecsClusterName, + targetGroupNames: targetGroupNames, + listenerArn: this.config.albConfig.listenerArn, + snsNotificationTopicArn: + this.config.codeDeploySnsNotificationTopicArn, + tags: this.config.tags, + dependsOn: [this.service], + successTerminationWaitTimeInMinutes: + this.config.successTerminationWaitTimeInMinutes, + notifications: this.config.codeDeployNotifications, + provider: this.config.provider, + })); + + if ( + !this.config.useCodePipeline && + this.config.useTerraformBasedCodeDeploy + ) { + /** + * If you make any changes to the Task Definition this must be called since we ignore changes to it. + * + * We typically ignore changes to the following since we rely on BlueGreen Deployments: + * ALB Default Action Target Group ARN + * ECS Service LoadBalancer Config + * ECS Task Definition + * ECS Placement Strategy Config + */ + const nullECSTaskUpdate = new Resource(this, 'update-task-definition', { + triggers: { task_arn: taskDef.arn }, + dependsOn: [ + taskDef, + codeDeployApp.codeDeployApp, + codeDeployApp.codeDeployDeploymentGroup, + ], + }); + + nullECSTaskUpdate.addOverride( + 'provisioner.local-exec.command', + `export app_spec_content_string='{"version":1,"Resources":[{"TargetService":{"Type":"AWS::ECS::Service","Properties":{"TaskDefinition":"${taskDef.arn}","LoadBalancerInfo":{"ContainerName":"${this.config.albConfig.containerName}","ContainerPort":${this.config.albConfig.containerPort}}}}}]}' && export revision="revisionType=AppSpecContent,appSpecContent={content='$app_spec_content_string'}" && aws --region ${this.config.region} deploy create-deployment --application-name="${codeDeployApp.codeDeployApp.name}" --deployment-group-name="${codeDeployApp.codeDeployDeploymentGroup.deploymentGroupName}" --description="Triggered from Terraform/CodeBuild due to a task definition update" --revision="$revision"`, + ); + } + + // We always create the appspec and taskdef files as long as we have an albConfig + if (config.albConfig) { + this.generateAppSpecAndTaskDefFiles(taskDef, config); + } + } + + // NEXT STEPS: + + // https://getpocket.atlassian.net/browse/BACK-411 + // build in autoscaling + } + + // set defaults on optional properties + private static hydrateConfig( + config: ApplicationECSServiceProps, + ): ApplicationECSServiceProps { + config.launchType = config.launchType || 'FARGATE'; + config.deploymentMinimumHealthyPercent = + config.deploymentMinimumHealthyPercent || 100; + config.deploymentMaximumPercent = config.deploymentMaximumPercent || 200; + config.desiredCount = config.desiredCount || 2; + //Need to use ?? because useCodeDeploy can be false + config.useCodeDeploy = config.useCodeDeploy ?? true; + config.useTerraformBasedCodeDeploy = + config.useTerraformBasedCodeDeploy ?? true; + + config.lifecycleIgnoreChanges = config.lifecycleIgnoreChanges || [ + 'desired_count', + 'load_balancer', + ]; + if (config.useCodeDeploy) { + // If we are using CodeDeploy then we need to ignore the task_definition change + config.lifecycleIgnoreChanges.push('task_definition'); + config.lifecycleIgnoreChanges = [ + // De-dupe array + ...new Set(config.lifecycleIgnoreChanges), + ]; + } + config.cpu = config.cpu || 512; + config.memory = config.memory || 2048; + + return config; + } + + /** + * When running ECS Blue/Green CodeDeploy through CodePipeline, the configuration requires that + * appspec.json and taskdef.json files exist within the source artifact. + * This function creates those files as part of the terraform stack + * @param taskDef + * @param config + * @private + */ + private generateAppSpecAndTaskDefFiles( + taskDef: EcsTaskDefinition, + config: ApplicationECSServiceProps, + ) { + if (config.useCodePipeline) { + const nullCreateTaskDef = new Resource( + this, + 'create-task-definition-file', + { + triggers: { + // Sets this null resorce to be triggered on every terraform apply + alwaysRun: Fn.timestamp(), + }, + dependsOn: [taskDef], + }, + ); + + // There is no way to pull the task def from the output of the terraform resource. + // Instead of trying to build a task def ourselves we use a null resource to access the recent version + // in AWS AFTER we have created our new one. + // It is also incredibly silly that AWS Codepipeline requires a task definition because it is already getting the + // Task definition ARN in the app spec file. But you know. Amazon is amazon and we must obey the law. + nullCreateTaskDef.addOverride( + 'provisioner.local-exec.command', + `aws --region ${this.config.region} ecs describe-task-definition --task-definition ${taskDef.family} --query 'taskDefinition' >> taskdef.json`, + ); + } + + new File(this, 'appspec', { + content: JSON.stringify({ + version: 1, + Resources: [ + { + TargetService: { + Type: 'AWS::ECS::Service', + Properties: { + TaskDefinition: taskDef.arn, + LoadBalancerInfo: { + ContainerName: config.albConfig.containerName, + ContainerPort: config.albConfig.containerPort, + }, + }, + }, + }, + ], + }), + filename: 'appspec.json', + }); + } + + /** + * Sets up the required ECS Security Groups + * @private + */ + private setupECSSecurityGroups(): SecurityGroup { + let ingress: SecurityGroupIngress[] = []; + if (this.config.albConfig) { + ingress = [ + { + fromPort: 80, + protocol: 'TCP', + toPort: this.config.albConfig.containerPort, + securityGroups: [this.config.albConfig.albSecurityGroupId], + description: 'required', + cidrBlocks: [], + ipv6CidrBlocks: [], + prefixListIds: [], + }, + ]; + } + + const egress: SecurityGroupEgress[] = [ + { + fromPort: 0, + protocol: '-1', + toPort: 0, + cidrBlocks: ['0.0.0.0/0'], + description: 'required', + ipv6CidrBlocks: [], + prefixListIds: [], + securityGroups: [], + }, + ]; + + return new SecurityGroup(this, `ecs_security_group`, { + namePrefix: `${this.config.prefix}-ECSSecurityGroup`, + description: 'Internal ECS Security Group (Managed by Terraform)', + vpcId: this.config.vpcId, + ingress, + egress, + tags: this.config.tags, + lifecycle: { + createBeforeDestroy: true, + }, + provider: this.config.provider, + }); + } + + /** + * Setup the ECS Task Definition + * @private + */ + private setupECSTaskDefinition(): ECSTaskDefinitionResponse { + const ecrRepos: EcrRepository[] = []; + + const containerDefs = []; + // Set of unique volumes by volume name + const volumes: { [key: string]: EcsTaskDefinitionVolume } = {}; + + // figure out if we need to create an ECR for each container definition + // also build a container definition JSON for each container + this.config.containerConfigs.forEach((def) => { + // if an image has been given, it must already live somewhere, so an ECR isn't needed + if (!def.containerImage) { + const ecrConfig: ECRProps = { + name: `${this.config.prefix}-${def.name}`.toLowerCase(), + tags: this.config.tags, + provider: this.config.provider, + }; + + const ecr = new ApplicationECR(this, `ecr-${def.name}`, ecrConfig); + //Set the image to the latest one for now + def.containerImage = `${ecr.repo.repositoryUrl}:latest`; + //The task and service need to depend on the repository existing. + ecrRepos.push(ecr.repo); + } + + // create log group if one not given + if (!def.logGroup) { + const cloudwatchLogGroup = new CloudwatchLogGroup( + this, + `ecs-${def.name}`, + { + namePrefix: `/ecs/${this.config.prefix}/${def.name}`, + retentionInDays: 30, + tags: this.config.tags, + provider: this.config.provider, + }, + ); + + def.logGroup = cloudwatchLogGroup.name; + def.logGroupRegion = this.config.region; + } + + if (def.mountPoints) { + def.mountPoints.forEach((mountPoint) => { + // We currently only set the volume names, but more configuration is available in EcsTaskDefinitionVolume. + volumes[mountPoint.sourceVolume] = { name: mountPoint.sourceVolume }; + if ( + this.config.efsConfig && + this.config.efsConfig.volumeName === mountPoint.sourceVolume + ) { + volumes[mountPoint.sourceVolume] = { + name: mountPoint.sourceVolume, + efsVolumeConfiguration: { + fileSystemId: this.config.efsConfig.efs.id, + }, + }; + } + }); + } + + containerDefs.push(buildDefinitionJSON(def)); + }); + + this.ecsIam = new ApplicationECSIAM(this, 'ecs-iam', { + prefix: this.config.prefix, + tags: this.config.tags, + taskExecutionDefaultAttachmentArn: + this.config.ecsIamConfig.taskExecutionDefaultAttachmentArn, + taskExecutionRolePolicyStatements: + this.config.ecsIamConfig.taskExecutionRolePolicyStatements, + taskRolePolicyStatements: + this.config.ecsIamConfig.taskRolePolicyStatements, + provider: this.config.provider, + }); + + //Create task definition + const taskDef = new EcsTaskDefinition(this, 'ecs-task', { + // why are container definitions just JSON? can we get a real construct? sheesh. + containerDefinitions: `[${containerDefs}]`, + family: `${this.config.prefix}`, + executionRoleArn: this.ecsIam.taskExecutionRoleArn, + taskRoleArn: this.ecsIam.taskRoleArn, + cpu: this.config.cpu.toString(), + memory: this.config.memory.toString(), + requiresCompatibilities: ['FARGATE'], + networkMode: 'awsvpc', + volume: Object.values(volumes), + tags: this.config.tags, + dependsOn: ecrRepos, + provider: this.config.provider, + }); + + if (this.config.efsConfig) { + this.efsFilePolicy( + this.config.efsConfig.efs, + this.ecsIam.taskRoleArn, + this.config.prefix, + ); + this.createEfsMount(this.config.efsConfig.efs); + } + return { taskDef, ecrRepos }; + } + + /** + * Helper function to get a target group + * @private + */ + private createTargetGroup(name: string): ApplicationTargetGroup { + return new ApplicationTargetGroup(this, `${name}_target_group`, { + shortName: truncateString(`${this.config.shortName}${name}`, 6), + vpcId: this.config.vpcId, + healthCheckPath: this.config.albConfig.healthCheckPath, + tags: { ...this.config.tags, type: name }, + provider: this.config.provider, + }); + } + + private createEfsMount(efsFs: EFSProps) { + const ingress: SecurityGroupIngress[] = [ + { + // EFS port is not configurable in AWS + fromPort: 2049, + protocol: 'TCP', + toPort: 2049, + securityGroups: [this.ecsSecurityGroup.id], + description: 'required', + cidrBlocks: [], + ipv6CidrBlocks: [], + prefixListIds: [], + }, + ]; + + const egress: SecurityGroupEgress[] = [ + { + fromPort: 0, + protocol: '-1', + toPort: 0, + cidrBlocks: ['0.0.0.0/0'], + description: 'required', + ipv6CidrBlocks: [], + prefixListIds: [], + securityGroups: [], + }, + ]; + + const mountSecurityGroup = new SecurityGroup(this, 'efs_mount_sg', { + namePrefix: `${this.config.prefix}-ECSSMountPoint`, + description: 'ECS EFS Mount (Managed by Terraform)', + vpcId: this.config.vpcId, + ingress, + egress, + tags: this.config.tags, + lifecycle: { + createBeforeDestroy: true, + }, + provider: this.config.provider, + }); + + // https://developer.hashicorp.com/terraform/cdktf/concepts/iterators + const iterator = TerraformIterator.fromList(this.config.privateSubnetIds); + + new EfsMountTarget(this, 'efs_mount_target', { + forEach: iterator, + fileSystemId: efsFs.id, + subnetId: iterator.value, + securityGroups: [mountSecurityGroup.id], + provider: this.config.provider, + }); + } + + private efsFilePolicy( + efsFs: EFSProps, + roleArn: string, + creationToken: string, + ) { + const FsPolicy = { + Version: '2012-10-17', + Id: creationToken, + Statement: [ + { + Sid: creationToken, + Effect: 'Allow', + Principal: { + AWS: '*', + }, + Resource: efsFs.arn, + Action: [ + 'elasticfilesystem:ClientMount', + 'elasticfilesystem:ClientWrite', + 'elasticfilesystem:ClientRootAccess', + ], + Condition: { + Bool: { + 'elasticfilesystem:AccessedViaMountTarget': 'true', + }, + }, + }, + ], + }; + + const waitTwoMinutes = new Sleep(this, 'waitTwoMinutes', { + createDuration: '2m', + dependsOn: [this.ecsIam.taskRoleArn], + }); + + new EfsFileSystemPolicy(this, 'efsFsPolicy', { + fileSystemId: efsFs.id, + policy: JSON.stringify(FsPolicy), + // https://github.com/hashicorp/terraform-provider-aws/pull/21734 + dependsOn: [waitTwoMinutes], + provider: this.config.provider, + }); + } +} diff --git a/packages/terraform-modules/src/base/ApplicationElasticacheCluster.ts b/packages/terraform-modules/src/base/ApplicationElasticacheCluster.ts new file mode 100644 index 00000000..68914781 --- /dev/null +++ b/packages/terraform-modules/src/base/ApplicationElasticacheCluster.ts @@ -0,0 +1,174 @@ +import { DataAwsVpc } from '@cdktf/provider-aws/lib/data-aws-vpc'; +import { ElasticacheSubnetGroup } from '@cdktf/provider-aws/lib/elasticache-subnet-group'; +import { SecurityGroup } from '@cdktf/provider-aws/lib/security-group'; +import { TerraformMetaArguments } from 'cdktf'; +import { Construct } from 'constructs'; + +export enum ApplicationElasticacheEngine { + MEMCACHED = 'memcached', + REDIS = 'redis', +} + +export interface ApplicationElasticacheClusterProps + extends TerraformMetaArguments { + prefix: string; + vpcId: string; + subnetIds: string[]; + allowedIngressSecurityGroupIds: string[]; + node?: { + /** + * This is the size as defined here:https://aws.amazon.com/elasticache/pricing + * Theres a lot here to make an enum for this.. + */ + size?: string; + /** + * Number of nodes that should exist in the cluster + */ + count?: number; + }; + tags?: { [key: string]: string }; + overrideEngineVersion?: string; // overrides lookup by engine type, below + overrideParameterGroupName?: string; // overides lookup by engine type, below +} + +/** + * Generates an elasticache cluster with the desired engine + */ +export abstract class ApplicationElasticacheCluster extends Construct { + protected constructor(scope: Construct, name: string) { + super(scope, name); + } + + /** + * Gets a VPC + * @param scope + * @param config + * @protected + */ + protected static getVpc( + scope: Construct, + config: ApplicationElasticacheClusterProps, + ): DataAwsVpc { + return new DataAwsVpc(scope, `vpc`, { + filter: [ + { + name: 'vpc-id', + values: [config.vpcId], + }, + ], + provider: config.provider, + }); + } + + /** + * Returns the default port for the selected engine + * @param engine + * @private + */ + protected static getPortForEngine( + engine: ApplicationElasticacheEngine, + ): number { + switch (engine) { + case ApplicationElasticacheEngine.MEMCACHED: + return 11211; + case ApplicationElasticacheEngine.REDIS: + return 6379; + } + } + + /** + * Returns the default parameter group for the selected engine + * @param engine + * @private + */ + protected static getParameterGroupForEngine( + engine: ApplicationElasticacheEngine, + ): string { + switch (engine) { + case ApplicationElasticacheEngine.MEMCACHED: + return 'default.memcached1.6'; + case ApplicationElasticacheEngine.REDIS: + return 'default.redis7'; + } + } + + /** + * Returns the default engine version for the selected engine + * @param engine + * @private + */ + protected static getEngineVersionForEngine( + engine: ApplicationElasticacheEngine, + ): string { + switch (engine) { + case ApplicationElasticacheEngine.MEMCACHED: + return '1.6.6'; + case ApplicationElasticacheEngine.REDIS: + return '7.0'; + } + } + + /** + * Create a security group and a subnet group for Elasticache + * @param scope + * @param config + * @param appVpc + * @param port + * @protected + */ + protected static createSecurityGroupAndSubnet( + scope: Construct, + config: ApplicationElasticacheClusterProps, + appVpc: DataAwsVpc, + port: number, + ): { + securityGroup: SecurityGroup; + subnetGroup: ElasticacheSubnetGroup; + } { + const securityGroup = new SecurityGroup( + scope, + 'elasticache_security_group', + { + namePrefix: config.prefix, + description: 'Managed by Terraform', + vpcId: appVpc.id, + ingress: [ + { + fromPort: port, + toPort: port, + protocol: 'tcp', + + //If we have a ingress security group it takes precedence + securityGroups: config.allowedIngressSecurityGroupIds + ? config.allowedIngressSecurityGroupIds + : null, + + //IF we do not have a ingress security group lets all the whole vpc access + cidrBlocks: config.allowedIngressSecurityGroupIds + ? null + : [appVpc.cidrBlock], + + // the following are included due to a bug + // https://github.com/hashicorp/terraform-cdk/issues/223 + description: null, + ipv6CidrBlocks: null, + prefixListIds: null, + }, + ], + tags: config.tags, + provider: config.provider, + }, + ); + const subnetGroup = new ElasticacheSubnetGroup( + scope, + 'elasticache_subnet_group', + { + name: `${config.prefix.toLowerCase()}-ElasticacheSubnetGroup`, + subnetIds: config.subnetIds, + provider: config.provider, + tags: config.tags, + }, + ); + return { securityGroup, subnetGroup }; + } +} diff --git a/packages/terraform-modules/src/base/ApplicationEventBridgeRule.spec.ts b/packages/terraform-modules/src/base/ApplicationEventBridgeRule.spec.ts new file mode 100644 index 00000000..fa69e5d4 --- /dev/null +++ b/packages/terraform-modules/src/base/ApplicationEventBridgeRule.spec.ts @@ -0,0 +1,104 @@ +import { SqsQueue } from '@cdktf/provider-aws/lib/sqs-queue'; +import { Testing } from 'cdktf'; +import { + ApplicationEventBridgeRule, + ApplicationEventBridgeRuleProps, +} from './ApplicationEventBridgeRule'; + +const config: ApplicationEventBridgeRuleProps = { + name: 'Test-EventBridge', + eventPattern: { + source: ['aws.states'], + 'detail-type': ['Step Functions Execution Status Change'], + }, +}; + +describe('AplicationEventBridgeRule', () => { + it('renders an event bridge without a target', () => { + const synthed = Testing.synthScope((stack) => { + new ApplicationEventBridgeRule(stack, 'test-event-bridge-rule', config); + }); + expect(synthed).toMatchSnapshot(); + }); + + it('renders an event bridge with sqs target', () => { + const synthed = Testing.synthScope((stack) => { + const appSqs = new SqsQueue(stack, 'test-queue', { + name: 'Test-SQS-Queue', + }); + + new ApplicationEventBridgeRule(stack, 'test-event-bridge-rule', { + ...config, + targets: [ + { + dependsOn: appSqs, + arn: appSqs.arn, + targetId: 'sqs', + }, + ], + }); + }); + expect(synthed).toMatchSnapshot(); + }); + + it('renders an event bridge with pre-existing sqs target', () => { + const synthed = Testing.synthScope((stack) => { + const appSqs = new SqsQueue(stack, 'test-queue', { + name: 'Test-SQS-Queue', + }); + + new ApplicationEventBridgeRule(stack, 'test-event-bridge-rule', { + ...config, + targets: [ + { + arn: appSqs.arn, + targetId: 'sqs', + }, + ], + }); + }); + expect(synthed).toMatchSnapshot(); + }); + + it('renders an event bridge with description', () => { + const synthed = Testing.synthScope((stack) => { + new ApplicationEventBridgeRule(stack, 'test-event-bridge-rule', { + ...config, + description: 'Test description', + }); + }); + expect(synthed).toMatchSnapshot(); + }); + + it('renders an event bridge with event bus name', () => { + const synthed = Testing.synthScope((stack) => { + new ApplicationEventBridgeRule(stack, 'test-event-bridge-rule', { + ...config, + eventBusName: 'test-bus', + }); + }); + expect(synthed).toMatchSnapshot(); + }); + + it('renders an event bridge with tags', () => { + const synthed = Testing.synthScope((stack) => { + new ApplicationEventBridgeRule(stack, 'test-event-bridge-rule', { + ...config, + tags: { + my: 'tag', + for: 'test', + }, + }); + }); + expect(synthed).toMatchSnapshot(); + }); + it('renders an event bridge with scheduleExpression', () => { + const synthed = Testing.synthScope((stack) => { + new ApplicationEventBridgeRule(stack, 'test-event-bridge-rule', { + name: 'Test-EventBridge', + scheduleExpression: 'rate(1 minute)', + }); + }); + expect(synthed).toMatchSnapshot(); + }); +}); diff --git a/packages/terraform-modules/src/base/ApplicationEventBridgeRule.ts b/packages/terraform-modules/src/base/ApplicationEventBridgeRule.ts new file mode 100644 index 00000000..c4356d7c --- /dev/null +++ b/packages/terraform-modules/src/base/ApplicationEventBridgeRule.ts @@ -0,0 +1,103 @@ +import { TerraformMetaArguments, TerraformResource } from 'cdktf'; +import { Construct } from 'constructs'; +import { CloudwatchEventRule } from '@cdktf/provider-aws/lib/cloudwatch-event-rule'; +import { + CloudwatchEventTarget, + CloudwatchEventTargetConfig, +} from '@cdktf/provider-aws/lib/cloudwatch-event-target'; + +export type Target = { + arn: string; + deadLetterArn?: string; + targetId?: string; + // an event bridge rule may have a target that already exists. in this case, + // we don't need a dependsOn value. + dependsOn?: TerraformResource; +}; + +export interface ApplicationEventBridgeRuleProps + extends TerraformMetaArguments { + name: string; + description?: string; + eventBusName?: string; + /** + * (Optional) The event pattern described a JSON object. + * At least one of `schedule_expression` or `event_pattern` is required. */ + eventPattern?: { [key: string]: any }; + /** + * (Optional) The scheduling expression. + * For example, cron(0 20 * * ? *) or rate(5 minutes). + * At least one of `schedule_expression` or `event_pattern` is required. + * Only available on the default event bus. */ + scheduleExpression?: string; + targets?: Target[]; + tags?: { [key: string]: string }; + preventDestroy?: boolean; +} + +export class ApplicationEventBridgeRule extends Construct { + public readonly rule: CloudwatchEventRule; + + constructor( + scope: Construct, + name: string, + private config: ApplicationEventBridgeRuleProps, + ) { + super(scope, name); + + this.rule = this.createCloudwatchEventRule(); + } + + private createCloudwatchEventRule() { + if (this.config.targets?.length > 5) { + throw new Error('AWS allows only up to 5 targets per event bridge rule'); + } + const eventBus = this.config.eventBusName ?? 'default'; + const { scheduleExpression, eventPattern } = this.config; + const rule = new CloudwatchEventRule(this, 'event-bridge-rule', { + name: `${this.config.name}-Rule`, + description: this.config.description, + eventPattern: eventPattern ? JSON.stringify(eventPattern) : undefined, + scheduleExpression, + eventBusName: eventBus, + lifecycle: { + preventDestroy: this.config.preventDestroy, + }, + tags: this.config.tags, + provider: this.config.provider, + }); + + if (this.config.targets) { + this.config.targets.forEach((t) => { + // this is semi-terrible. + // + // the CloudwatchEventTargetConfig type will not allow us to add the + // `dependsOn` property after object creation (due to it being + // readonly). so we create a generic object, optionally add the + // `dependsOn` property, then later cast it as the required + // CloudwatchEventTargetConfig type. + const eventTargetConfig: { [key: string]: any } = { + rule: rule.name, + targetId: t.targetId, + arn: t.arn, + deadLetterConfig: t.deadLetterArn ? { arn: t.deadLetterArn } : {}, + eventBusName: eventBus, + provider: this.config.provider, + }; + + if (t.dependsOn) { + eventTargetConfig.dependsOn = [t.dependsOn, rule]; + } + + new CloudwatchEventTarget( + this, + `event-bridge-target-${t.targetId}`, + // yuck! + eventTargetConfig as CloudwatchEventTargetConfig, + ); + }); + } + + return rule; + } +} diff --git a/packages/terraform-modules/src/base/ApplicationEventBus.spec.ts b/packages/terraform-modules/src/base/ApplicationEventBus.spec.ts new file mode 100644 index 00000000..d1f89ca4 --- /dev/null +++ b/packages/terraform-modules/src/base/ApplicationEventBus.spec.ts @@ -0,0 +1,18 @@ +import { Testing } from 'cdktf'; +import { + ApplicationEventBus, + ApplicationEventBusProps, +} from './ApplicationEventBus'; + +describe('ApplicationEventBus', () => { + it('renders an event bus with name and target', () => { + const props: ApplicationEventBusProps = { + name: 'test-event-bus', + tags: { service: 'test-service' }, + }; + const synthed = Testing.synthScope((stack) => { + new ApplicationEventBus(stack, 'test-event-bus', props); + }); + expect(synthed).toMatchSnapshot(); + }); +}); diff --git a/packages/terraform-modules/src/base/ApplicationEventBus.ts b/packages/terraform-modules/src/base/ApplicationEventBus.ts new file mode 100644 index 00000000..730f68ac --- /dev/null +++ b/packages/terraform-modules/src/base/ApplicationEventBus.ts @@ -0,0 +1,30 @@ +import { CloudwatchEventBus } from '@cdktf/provider-aws/lib/cloudwatch-event-bus'; +import { TerraformMetaArguments } from 'cdktf'; +import { Construct } from 'constructs'; + +export interface ApplicationEventBusProps extends TerraformMetaArguments { + name: string; + tags?: { [key: string]: string }; +} + +export class ApplicationEventBus extends Construct { + public readonly bus: CloudwatchEventBus; + + constructor( + scope: Construct, + name: string, + private config: ApplicationEventBusProps, + ) { + super(scope, name); + + this.bus = this.createCloudWatchEventBus(); + } + + private createCloudWatchEventBus(): CloudwatchEventBus { + return new CloudwatchEventBus(this, `event-bus-${this.config.name}`, { + name: this.config.name, + tags: this.config.tags, + provider: this.config.provider, + }); + } +} diff --git a/packages/terraform-modules/src/base/ApplicationLambdaCodeDeploy.spec.ts b/packages/terraform-modules/src/base/ApplicationLambdaCodeDeploy.spec.ts new file mode 100644 index 00000000..ba46699e --- /dev/null +++ b/packages/terraform-modules/src/base/ApplicationLambdaCodeDeploy.spec.ts @@ -0,0 +1,82 @@ +import { Testing } from 'cdktf'; +import { + ApplicationLambdaCodeDeploy, + ApplicationVersionedLambdaCodeDeployProps, +} from './ApplicationLambdaCodeDeploy'; + +const config: ApplicationVersionedLambdaCodeDeployProps = { + name: 'Test-Lambda-Code-Deploy', + region: 'us-east-1', + accountId: '123', +}; + +describe('ApplicationLambdaCodeDeploy', () => { + it('renders a lambda code deploy app', () => { + const synthed = Testing.synthScope((stack) => { + new ApplicationLambdaCodeDeploy(stack, 'test-lambda-code-deploy', config); + }); + expect(synthed).toMatchSnapshot(); + }); + + it('renders a lambda code deploy app with sns topic arn', () => { + const synthed = Testing.synthScope((stack) => { + new ApplicationLambdaCodeDeploy(stack, 'test-lambda-code-deploy', { + ...config, + deploySnsTopicArn: 'test:deploy-topic:arn', + }); + }); + expect(synthed).toMatchSnapshot(); + }); + + it('renders a lambda code deploy app with sns topic arn and detail type', () => { + const synthed = Testing.synthScope((stack) => { + new ApplicationLambdaCodeDeploy(stack, 'test-lambda-code-deploy', { + ...config, + deploySnsTopicArn: 'test:deploy-topic:arn', + detailType: 'FULL', + }); + }); + expect(synthed).toMatchSnapshot(); + }); + + it('renders a lambda code deploy app with default notifications', () => { + const synthed = Testing.synthScope((stack) => { + new ApplicationLambdaCodeDeploy(stack, 'test-lambda-code-deploy', { + ...config, + deploySnsTopicArn: 'test:deploy-topic:arn', + }); + }); + expect(synthed).toMatchSnapshot(); + }); + + it('renders a lambda code deploy app with started and succeeded notifications only', () => { + const synthed = Testing.synthScope((stack) => { + new ApplicationLambdaCodeDeploy(stack, 'test-lambda-code-deploy', { + ...config, + deploySnsTopicArn: 'test:deploy-topic:arn', + notifications: { + //have to explicitly set notifyOnFailed to false since it'll be set to true as default + notifyOnFailed: false, + notifyOnStarted: true, + notifyOnSucceeded: true, + }, + }); + }); + expect(synthed).toMatchSnapshot(); + }); + + it('renders a lambda code deploy app with all notifications enabled', () => { + const synthed = Testing.synthScope((stack) => { + new ApplicationLambdaCodeDeploy(stack, 'test-lambda-code-deploy', { + ...config, + deploySnsTopicArn: 'test:deploy-topic:arn', + notifications: { + //not setting the notifyOnFailed since it will be enabled by default if not provided + notifyOnStarted: true, + notifyOnSucceeded: true, + }, + }); + }); + expect(synthed).toMatchSnapshot(); + }); +}); diff --git a/packages/terraform-modules/src/base/ApplicationLambdaCodeDeploy.ts b/packages/terraform-modules/src/base/ApplicationLambdaCodeDeploy.ts new file mode 100644 index 00000000..26893e42 --- /dev/null +++ b/packages/terraform-modules/src/base/ApplicationLambdaCodeDeploy.ts @@ -0,0 +1,158 @@ +import { CodedeployApp } from '@cdktf/provider-aws/lib/codedeploy-app'; +import { CodedeployDeploymentGroup } from '@cdktf/provider-aws/lib/codedeploy-deployment-group'; +import { CodestarnotificationsNotificationRule } from '@cdktf/provider-aws/lib/codestarnotifications-notification-rule'; +import { DataAwsIamPolicyDocument } from '@cdktf/provider-aws/lib/data-aws-iam-policy-document'; +import { IamRole } from '@cdktf/provider-aws/lib/iam-role'; +import { IamRolePolicyAttachment } from '@cdktf/provider-aws/lib/iam-role-policy-attachment'; +import { TerraformMetaArguments } from 'cdktf'; +import { Construct } from 'constructs'; + +export interface ApplicationVersionedLambdaCodeDeployProps + extends TerraformMetaArguments { + name: string; + deploySnsTopicArn?: string; + detailType?: 'BASIC' | 'FULL'; + region: string; + accountId: string; + tags?: { [key: string]: string }; + notifications?: { + /** + * Option to send CodeDeploy notifications on Started event, defaults to false. + */ + notifyOnStarted?: boolean; + /** + * Option to send CodeDeploy notifications on Succeeded event, defaults to false. + */ + notifyOnSucceeded?: boolean; + /** + * Option to send CodeDeploy notifications on Failed event, defaults to true. + */ + notifyOnFailed?: boolean; + }; +} + +export class ApplicationLambdaCodeDeploy extends Construct { + public readonly codeDeployApp: CodedeployApp; + + constructor( + scope: Construct, + name: string, + private config: ApplicationVersionedLambdaCodeDeployProps, + ) { + super(scope, name); + + this.codeDeployApp = this.setupCodeDeploy(); + } + + private setupCodeDeploy() { + const codeDeployApp = new CodedeployApp(this, 'code-deploy-app', { + name: `${this.config.name}-Lambda`, + computePlatform: 'Lambda', + provider: this.config.provider, + tags: this.config.tags, + }); + + this.createCodeDeploymentGroup(codeDeployApp); + + if (this.config.deploySnsTopicArn) { + this.setupCodeDeployNotifications(codeDeployApp); + } + + return codeDeployApp; + } + + private createCodeDeploymentGroup(codeDeployApp: CodedeployApp) { + new CodedeployDeploymentGroup(this, 'code-deployment-group', { + appName: codeDeployApp.name, + deploymentConfigName: 'CodeDeployDefault.LambdaAllAtOnce', + deploymentGroupName: codeDeployApp.name, + serviceRoleArn: this.getCodeDeployRole().arn, + deploymentStyle: { + deploymentType: 'BLUE_GREEN', + deploymentOption: 'WITH_TRAFFIC_CONTROL', + }, + autoRollbackConfiguration: { + enabled: true, + events: ['DEPLOYMENT_FAILURE'], + }, + dependsOn: [codeDeployApp], + provider: this.config.provider, + tags: this.config.tags, + }); + } + + private getCodeDeployRole() { + const codeDeployRole = new IamRole(this, 'code-deploy-role', { + name: `${this.config.name}-CodeDeployRole`, + assumeRolePolicy: this.getCodeDeployAssumePolicyDocument(), + provider: this.config.provider, + tags: this.config.tags, + }); + + new IamRolePolicyAttachment(this, 'code-deploy-policy-attachment', { + policyArn: + 'arn:aws:iam::aws:policy/service-role/AWSCodeDeployRoleForLambda', + role: codeDeployRole.name, + dependsOn: [codeDeployRole], + provider: this.config.provider, + }); + + return codeDeployRole; + } + + private getCodeDeployAssumePolicyDocument() { + return new DataAwsIamPolicyDocument( + this, + 'code-deploy-assume-role-policy-document', + { + statement: [ + { + effect: 'Allow', + actions: ['sts:AssumeRole'], + principals: [ + { + identifiers: ['codedeploy.amazonaws.com'], + type: 'Service', + }, + ], + }, + ], + provider: this.config.provider, + }, + ).json; + } + + private setupCodeDeployNotifications(codeDeployApp: CodedeployApp) { + const eventTypeIds = []; + + // the OR condition here sets the notification for failed deploys which is a default when no notification preferences are provided + if ( + this.config.notifications?.notifyOnFailed || + this.config.notifications?.notifyOnFailed === undefined + ) { + eventTypeIds.push('codedeploy-application-deployment-failed'); + } + + if (this.config.notifications?.notifyOnSucceeded) { + eventTypeIds.push('codedeploy-application-deployment-succeeded'); + } + + if (this.config.notifications?.notifyOnStarted) { + eventTypeIds.push('codedeploy-application-deployment-started'); + } + + new CodestarnotificationsNotificationRule(this, 'notifications', { + detailType: this.config.detailType ?? 'BASIC', + eventTypeIds: eventTypeIds, + name: codeDeployApp.name, + resource: `arn:aws:codedeploy:${this.config.region}:${this.config.accountId}:application:${codeDeployApp.name}`, + target: [ + { + address: this.config.deploySnsTopicArn, + }, + ], + provider: this.config.provider, + tags: this.config.tags, + }); + } +} diff --git a/packages/terraform-modules/src/base/ApplicationLambdaSnsTopicSubscription.spec.ts b/packages/terraform-modules/src/base/ApplicationLambdaSnsTopicSubscription.spec.ts new file mode 100644 index 00000000..6e7d1186 --- /dev/null +++ b/packages/terraform-modules/src/base/ApplicationLambdaSnsTopicSubscription.spec.ts @@ -0,0 +1,38 @@ +import { DataAwsLambdaFunction } from '@cdktf/provider-aws/lib/data-aws-lambda-function'; +import { Testing } from 'cdktf'; +import { ApplicationLambdaSnsTopicSubscription } from './ApplicationLambdaSnsTopicSubscription'; + +describe('ApplicationSqsSnsTopicSubscription', () => { + const getConfig = (stack) => ({ + name: 'test-sns-subscription', + snsTopicArn: 'arn:aws:sns:TopicName', + lambda: new DataAwsLambdaFunction(stack, 'lambda', { + functionName: 'test-lambda', + }), + }); + + it('renders an Lambda <> SNS subscription without tags', () => { + const synthed = Testing.synthScope((stack) => { + new ApplicationLambdaSnsTopicSubscription( + stack, + 'lambda-sns-subscription', + getConfig(stack), + ); + }); + expect(synthed).toMatchSnapshot(); + }); + + it('renders an SQS SNS subscription with tags', () => { + const synthed = Testing.synthScope((stack) => { + new ApplicationLambdaSnsTopicSubscription( + stack, + 'lambda-sns-subscription', + { + ...getConfig(stack), + tags: { hello: 'there' }, + }, + ); + }); + expect(synthed).toMatchSnapshot(); + }); +}); diff --git a/packages/terraform-modules/src/base/ApplicationLambdaSnsTopicSubscription.ts b/packages/terraform-modules/src/base/ApplicationLambdaSnsTopicSubscription.ts new file mode 100644 index 00000000..fb1ca9e4 --- /dev/null +++ b/packages/terraform-modules/src/base/ApplicationLambdaSnsTopicSubscription.ts @@ -0,0 +1,155 @@ +import { DataAwsIamPolicyDocument } from '@cdktf/provider-aws/lib/data-aws-iam-policy-document'; +import { DataAwsLambdaFunction } from '@cdktf/provider-aws/lib/data-aws-lambda-function'; +import { LambdaFunction } from '@cdktf/provider-aws/lib/lambda-function'; +import { LambdaPermission } from '@cdktf/provider-aws/lib/lambda-permission'; +import { + SnsTopicSubscription, + SnsTopicSubscriptionConfig, +} from '@cdktf/provider-aws/lib/sns-topic-subscription'; +import { SqsQueue } from '@cdktf/provider-aws/lib/sqs-queue'; +import { SqsQueuePolicy } from '@cdktf/provider-aws/lib/sqs-queue-policy'; +import { TerraformMetaArguments, TerraformResource } from 'cdktf'; +import { Construct } from 'constructs'; + +/** The config props type of [[`ApplicationLambdaSnsTopicSubscription]] */ +export interface ApplicationLambdaSnsTopicSubscriptionProps + extends TerraformMetaArguments { + /** The prefix used to help identify related resources */ + name: string; + /** The SNS topic to subscribe the Lambda to */ + snsTopicArn: string; + /** The Lambda that should be invoked by incoming messages to the SNS topic */ + lambda: DataAwsLambdaFunction | LambdaFunction; + /** Tags to apply to the resource(s), where applicable (in this case only the DLQ for the SNS) */ + tags?: { [key: string]: string }; + /** Optional list of resource dependencies */ + dependsOn?: TerraformResource[]; +} + +/** + * Creates an SNS to Lambda subscription with a DLQ for any messages + * that failed to send to the Lambda (e.g. due to permissions error). + * Automatically adds policies/permissions for the SNS topic to send + * messages to the DLQ and invoke the Lambda function. + * + * Artifacts: + * * {@link https://www.terraform.io/docs/providers/aws/r/sns_topic_subscription aws_sns_topic_subscription} Resource to subscribe Lambda to SNS topic + * * {@link https://www.terraform.io/docs/providers/aws/r/sqs_queue aws_sqs_queue} Resource (DLQ for the SNS topic) + * * {@link https://www.terraform.io/docs/providers/aws/r/sqs_queue_policy aws_sqs_queue_policy} Resource policy for SNS to send messages to DLQ + * * {@link https://www.terraform.io/docs/providers/aws/r/lambda_permission aws_lambda_permission} Resource permission for SNS to invoke Lambda + */ +export class ApplicationLambdaSnsTopicSubscription extends Construct { + /** the {@link https://www.terraform.io/docs/providers/aws/r/sns_topic_subscription aws_sns_topic_subscription} resource */ + public readonly snsTopicSubscription: SnsTopicSubscription; + /** the {@link https://www.terraform.io/docs/providers/aws/r/sqs_queue aws_sqs_queue} (DLQ) resource */ + public readonly snsTopicDlq: SqsQueue; + + constructor( + scope: Construct, + private name: string, + private config: ApplicationLambdaSnsTopicSubscriptionProps, + ) { + super(scope, name); + + this.snsTopicDlq = this.createSqsSubscriptionDlq(); + this.snsTopicSubscription = this.createSnsTopicSubscription( + this.snsTopicDlq, + ); + this.createDlqPolicy(this.snsTopicDlq); + this.createLambdaPolicy(); + } + + /** + * Create a dead-letter queue for failed SNS messages + * @private + */ + private createSqsSubscriptionDlq(): SqsQueue { + return new SqsQueue(this, 'sns-topic-dlq', { + name: `${this.config.name}-SNS-Topic-DLQ`, + tags: this.config.tags, + provider: this.config.provider, + }); + } + + /** + * Create an SNS subscription for Lambda + * @param snsTopicDlq the DLQ for messages that failed to be processed + * @private + */ + private createSnsTopicSubscription( + snsTopicDlq: SqsQueue, + ): SnsTopicSubscription { + return new SnsTopicSubscription(this, 'sns-subscription', { + topicArn: this.config.snsTopicArn, + protocol: 'lambda', + endpoint: this.config.lambda.arn, + redrivePolicy: JSON.stringify({ + deadLetterTargetArn: snsTopicDlq.arn, + }), + dependsOn: [ + snsTopicDlq, + this.config.lambda.arn, + ...(this.config.dependsOn ? this.config.dependsOn : []), + ], + provider: this.config.provider, + tags: this.config.tags, + } as SnsTopicSubscriptionConfig); + } + + /** + * Grant permissions for SNS topic to invoke lambda + * Cannot be applied to an alias; must use the base lambda function + */ + private createLambdaPolicy(): void { + new LambdaPermission(this, `${this.name}-lambda-permission`, { + principal: 'sns.amazonaws.com', + action: 'lambda:InvokeFunction', + functionName: this.config.lambda.functionName, + sourceArn: this.config.snsTopicArn, + provider: this.config.provider, + }); + } + + /** + * Create IAM policies to allow SNS write to the dead-letter queue + * @param snsTopicDlq the SQS resource (used as DLQ) to grant permissions on + * @private + */ + private createDlqPolicy(snsTopicDlq: SqsQueue): void { + const queue = { name: 'sns-dlq', resource: snsTopicDlq }; + const policy = new DataAwsIamPolicyDocument( + this, + `${queue.name}-policy-document`, + { + statement: [ + { + effect: 'Allow', + actions: ['sqs:SendMessage'], + resources: [queue.resource.arn], + principals: [ + { + identifiers: ['sns.amazonaws.com'], + type: 'Service', + }, + ], + condition: [ + { + test: 'ArnEquals', + variable: 'aws:SourceArn', + values: [this.config.snsTopicArn], + }, + ], + }, + ], + dependsOn: [queue.resource] as TerraformResource[], + provider: this.config.provider, + }, + ).json; + + new SqsQueuePolicy(this, `${queue.name}-policy`, { + queueUrl: queue.resource.url, + policy: policy, + provider: this.config.provider, + }); + } +} diff --git a/packages/terraform-modules/src/base/ApplicationLoadBalancer.spec.ts b/packages/terraform-modules/src/base/ApplicationLoadBalancer.spec.ts new file mode 100644 index 00000000..15215d6c --- /dev/null +++ b/packages/terraform-modules/src/base/ApplicationLoadBalancer.spec.ts @@ -0,0 +1,132 @@ +import { Testing } from 'cdktf'; +import { ApplicationLoadBalancer } from './ApplicationLoadBalancer'; + +describe('ApplicationLoadBalancer', () => { + it('renders an ALB without tags', () => { + const synthed = Testing.synthScope((stack) => { + new ApplicationLoadBalancer(stack, 'testALB', { + prefix: 'test-', + alb6CharacterPrefix: 'TEST', + vpcId: '123', + subnetIds: ['a', 'b'], + }); + }); + expect(synthed).toMatchSnapshot(); + }); + + it('renders an ALB with tags', () => { + const synthed = Testing.synthScope((stack) => { + new ApplicationLoadBalancer(stack, 'testALB', { + prefix: 'test-', + alb6CharacterPrefix: 'TEST', + vpcId: '123', + subnetIds: ['a', 'b'], + tags: { + name: 'thedude', + hobby: 'bowling', + }, + }); + }); + expect(synthed).toMatchSnapshot(); + }); + + it('renders an ALB with logs with a new bucket', () => { + const synthed = Testing.synthScope((stack) => { + new ApplicationLoadBalancer(stack, 'testALB', { + prefix: 'test-', + alb6CharacterPrefix: 'TEST', + vpcId: '123', + subnetIds: ['a', 'b'], + tags: { + name: 'thedude', + hobby: 'bowling', + }, + accessLogs: { + bucket: 'logging-bucket', + }, + }); + }); + expect(synthed).toMatchSnapshot(); + }); + + it('renders an ALB with logs with a new bucket and prefix', () => { + const synthed = Testing.synthScope((stack) => { + new ApplicationLoadBalancer(stack, 'testALB', { + prefix: 'test-', + alb6CharacterPrefix: 'TEST', + vpcId: '123', + subnetIds: ['a', 'b'], + tags: { + name: 'thedude', + hobby: 'bowling', + }, + accessLogs: { + bucket: 'logging-bucket', + prefix: 'logs/ahoy/cool/service', + }, + }); + }); + expect(synthed).toMatchSnapshot(); + }); + + it('renders an ALB with logs with an existing bucket', () => { + const synthed = Testing.synthScope((stack) => { + new ApplicationLoadBalancer(stack, 'testALB', { + prefix: 'test-', + alb6CharacterPrefix: 'TEST', + vpcId: '123', + subnetIds: ['a', 'b'], + tags: { + name: 'thedude', + hobby: 'bowling', + }, + accessLogs: { + existingBucket: 'logging-bucket', + }, + }); + }); + expect(synthed).toMatchSnapshot(); + }); + + it('renders an ALB with logs with an existing bucket and prefix', () => { + const synthed = Testing.synthScope((stack) => { + new ApplicationLoadBalancer(stack, 'testALB', { + prefix: 'test-', + alb6CharacterPrefix: 'TEST', + vpcId: '123', + subnetIds: ['a', 'b'], + tags: { + name: 'thedude', + hobby: 'bowling', + }, + accessLogs: { + existingBucket: 'logging-bucket', + prefix: 'logs/ahoy/cool/service', + }, + }); + }); + expect(synthed).toMatchSnapshot(); + }); + + it('errors when no bucket provided', () => { + expect(() => { + Testing.synthScope((stack) => { + new ApplicationLoadBalancer(stack, 'testALB', { + prefix: 'test-', + alb6CharacterPrefix: 'TEST', + vpcId: '123', + subnetIds: ['a', 'b'], + tags: { + name: 'thedude', + hobby: 'bowling', + }, + accessLogs: { + prefix: 'logs/ahoy/cool/service', + }, + }); + }); + }).toThrow( + 'If you are configuring access logs you need to define either an existing bucket or a new one to store the logs', + ); + }); +}); diff --git a/packages/terraform-modules/src/base/ApplicationLoadBalancer.ts b/packages/terraform-modules/src/base/ApplicationLoadBalancer.ts new file mode 100644 index 00000000..24802115 --- /dev/null +++ b/packages/terraform-modules/src/base/ApplicationLoadBalancer.ts @@ -0,0 +1,201 @@ +import { Alb, AlbAccessLogs, AlbConfig } from '@cdktf/provider-aws/lib/alb'; +import { DataAwsElbServiceAccount } from '@cdktf/provider-aws/lib/data-aws-elb-service-account'; +import { DataAwsIamPolicyDocument } from '@cdktf/provider-aws/lib/data-aws-iam-policy-document'; +import { DataAwsS3Bucket } from '@cdktf/provider-aws/lib/data-aws-s3-bucket'; +import { S3Bucket } from '@cdktf/provider-aws/lib/s3-bucket'; +import { S3BucketPolicy } from '@cdktf/provider-aws/lib/s3-bucket-policy'; +import { SecurityGroup } from '@cdktf/provider-aws/lib/security-group'; +import { TerraformMetaArguments, TerraformProvider } from 'cdktf'; +import { Construct } from 'constructs'; + +export interface ApplicationLoadBalancerProps extends TerraformMetaArguments { + prefix: string; + alb6CharacterPrefix: string; + vpcId: string; + subnetIds: string[]; + internal?: boolean; + /** + * Optional config to dump alb logs to a bucket. + */ + accessLogs?: { + /** + * Existing bucket to dump alb logs too, one of existingBucket or bucket must be chosen. + */ + existingBucket?: string; + + /** + * Bucket to dump alb logs too, one of existingBucket or bucket must be chosen. + */ + bucket?: string; + + /** + * Optional bucket path prefix. If not defined will use server-logs/{service-name}/internal-alb/AWSLogs/{awsaccountid}/elasticloadbalancing/ + * Be sure to include a trailing / + */ + prefix?: string; + }; + tags?: { [key: string]: string }; +} + +/** + * Generates an Application Certificate given a domain name and zoneId + */ +export class ApplicationLoadBalancer extends Construct { + public readonly alb: Alb; + public readonly securityGroup: SecurityGroup; + + constructor( + scope: Construct, + name: string, + config: ApplicationLoadBalancerProps, + ) { + super(scope, name); + + this.securityGroup = new SecurityGroup(this, `alb_security_group`, { + namePrefix: `${config.prefix}-HTTP/S Security Group`, + description: 'External security group (Managed by Terraform)', + vpcId: config.vpcId, + ingress: [ + { + fromPort: 443, + toPort: 443, + protocol: 'TCP', + cidrBlocks: ['0.0.0.0/0'], + }, + { + fromPort: 80, + toPort: 80, + protocol: 'TCP', + cidrBlocks: ['0.0.0.0/0'], + }, + ], + egress: [ + { + fromPort: 0, + protocol: '-1', + toPort: 0, + cidrBlocks: ['0.0.0.0/0'], + description: 'required', + ipv6CidrBlocks: [], + prefixListIds: [], + securityGroups: [], + }, + ], + tags: { + ...config.tags, + Name: `${config.prefix}-HTTP/S Security Group`, + }, + lifecycle: { + createBeforeDestroy: true, + }, + provider: config.provider, + }); + + let logsConfig: AlbAccessLogs = undefined; + if (config.accessLogs !== undefined) { + const defaultPrefix = `server-logs/${config.prefix.toLowerCase()}/alb`; + + const prefix = + config.accessLogs.prefix === undefined + ? defaultPrefix + : config.accessLogs.prefix; + + if ( + prefix.charAt(prefix.length - 1) === '/' || + prefix.charAt(0) === '/' + ) { + throw new Error("Logs prefix cannot start or end with '/'"); + } + + const bucket = this.getOrCreateBucket({ + bucket: config.accessLogs.bucket, + existingBucket: config.accessLogs.existingBucket, + provider: config.provider, + tags: config.tags, + }); + + logsConfig = { + bucket, + enabled: true, + prefix, + }; + } + + const albConfig: AlbConfig = { + namePrefix: config.alb6CharacterPrefix, + securityGroups: [this.securityGroup.id], + internal: config.internal !== undefined ? config.internal : false, + subnets: config.subnetIds, + tags: config.tags, + accessLogs: logsConfig, + provider: config.provider, + }; + this.alb = new Alb(this, `alb`, albConfig); + } + + /** + * + * @param config Creates a bucket according to https://docs.aws.amazon.com/elasticloadbalancing/latest/application/enable-access-logging.html#attach-bucket-policy if one does not exist + * @returns + */ + private getOrCreateBucket(config: { + existingBucket?: string; + bucket?: string; + tags?: { [key: string]: string }; + provider?: TerraformProvider; + }): string { + if (config.existingBucket === undefined && config.bucket === undefined) { + throw new Error( + 'If you are configuring access logs you need to define either an existing bucket or a new one to store the logs', + ); + } + + if (config.existingBucket !== undefined) { + return new DataAwsS3Bucket(this, 'log-bucket', { + bucket: config.existingBucket, + provider: config.provider, + }).bucket; + } + + const s3Bucket = new S3Bucket(this, 'log-bucket', { + bucket: config.bucket, + provider: config.provider, + tags: config.tags, + }); + + const albAccountId = new DataAwsElbServiceAccount( + this, + 'elb-service-account', + { provider: config.provider }, + ).id; + + const s3IAMDocument = new DataAwsIamPolicyDocument( + this, + 'iam-log-bucket-policy-document', + { + statement: [ + { + effect: 'Allow', + principals: [ + { + type: 'AWS', + identifiers: [`arn:aws:iam::${albAccountId}:root`], + }, + ], + actions: ['s3:PutObject'], + resources: [`arn:aws:s3:::${s3Bucket.bucket}/*`], + }, + ], + provider: config.provider, + }, + ); + + new S3BucketPolicy(this, 'log-bucket-policy', { + bucket: s3Bucket.bucket, + policy: s3IAMDocument.json, + provider: config.provider, + }); + + return s3Bucket.bucket; + } +} diff --git a/packages/terraform-modules/src/base/ApplicationMemcache.spec.ts b/packages/terraform-modules/src/base/ApplicationMemcache.spec.ts new file mode 100644 index 00000000..48a1a5f8 --- /dev/null +++ b/packages/terraform-modules/src/base/ApplicationMemcache.spec.ts @@ -0,0 +1,48 @@ +import { Testing } from 'cdktf'; +import { ApplicationElasticacheClusterProps } from './ApplicationElasticacheCluster'; + +import { ApplicationMemcache } from './ApplicationMemcache'; + +const BASE_CONFIG: ApplicationElasticacheClusterProps = { + allowedIngressSecurityGroupIds: [], + subnetIds: ['1234-123'], + vpcId: 'cool-vpc', + prefix: `abides-dev`, +}; + +describe('ApplicationMemcache', () => { + it('renders memcached with minimal config', () => { + const synthed = Testing.synthScope((stack) => { + new ApplicationMemcache(stack, 'testMemcached', BASE_CONFIG); + }); + expect(synthed).toMatchSnapshot(); + }); + + it('renders memcached with tags', () => { + const config: ApplicationElasticacheClusterProps = { + ...BASE_CONFIG, + tags: { + letsgo: 'bowling', + donnie: 'throwinrockstonight', + }, + }; + const synthed = Testing.synthScope((stack) => { + new ApplicationMemcache(stack, 'testMemcached', config); + }); + expect(synthed).toMatchSnapshot(); + }); + + it('renders memcached with node change', () => { + const config: ApplicationElasticacheClusterProps = { + ...BASE_CONFIG, + node: { + size: 'cache.m4.2xlarge', + count: 5, + }, + }; + const synthed = Testing.synthScope((stack) => { + new ApplicationMemcache(stack, 'testMemcached', config); + }); + expect(synthed).toMatchSnapshot(); + }); +}); diff --git a/packages/terraform-modules/src/base/ApplicationMemcache.ts b/packages/terraform-modules/src/base/ApplicationMemcache.ts new file mode 100644 index 00000000..85c4cb2b --- /dev/null +++ b/packages/terraform-modules/src/base/ApplicationMemcache.ts @@ -0,0 +1,82 @@ +import { + ApplicationElasticacheCluster, + ApplicationElasticacheClusterProps, + ApplicationElasticacheEngine, +} from './ApplicationElasticacheCluster'; +import { Construct } from 'constructs'; +import { DataAwsVpc } from '@cdktf/provider-aws/lib/data-aws-vpc'; +import { ElasticacheCluster } from '@cdktf/provider-aws/lib/elasticache-cluster'; + +const DEFAULT_CONFIG = { + node: { + size: 'cache.t2.micro', + count: 2, + }, +}; + +export class ApplicationMemcache extends ApplicationElasticacheCluster { + public elasticacheCluster: ElasticacheCluster; + + constructor( + scope: Construct, + name: string, + config: ApplicationElasticacheClusterProps, + ) { + super(scope, name); + + // use default config, but update any user-provided values + config = { + ...DEFAULT_CONFIG, + ...config, + }; + + const appVpc = ApplicationElasticacheCluster.getVpc(this, config); + + this.elasticacheCluster = ApplicationMemcache.createElasticacheCluster( + this, + appVpc, + config, + ); + } + + /** + * Creates the elasticache cluster to be used + * @param scope + * @param appVpc + * @param config + * @private + */ + private static createElasticacheCluster( + scope: Construct, + appVpc: DataAwsVpc, + config: ApplicationElasticacheClusterProps, + ): ElasticacheCluster { + const engine = ApplicationElasticacheEngine.MEMCACHED; + const port = ApplicationElasticacheCluster.getPortForEngine(engine); + + const { securityGroup, subnetGroup } = this.createSecurityGroupAndSubnet( + scope, + config, + appVpc, + port, + ); + + return new ElasticacheCluster(scope, `elasticache_cluster`, { + clusterId: config.prefix.toLowerCase(), + engine: engine.toString(), + nodeType: config.node.size, + numCacheNodes: config.node.count, + parameterGroupName: + ApplicationMemcache.getParameterGroupForEngine(engine), + port: port, + engineVersion: + ApplicationElasticacheCluster.getEngineVersionForEngine(engine), + subnetGroupName: subnetGroup.name, + securityGroupIds: [securityGroup.id], + tags: config.tags, + applyImmediately: true, + dependsOn: [subnetGroup, securityGroup], + provider: config.provider, + }); + } +} diff --git a/packages/terraform-modules/src/base/ApplicationRDSCluster.spec.ts b/packages/terraform-modules/src/base/ApplicationRDSCluster.spec.ts new file mode 100644 index 00000000..5cf45512 --- /dev/null +++ b/packages/terraform-modules/src/base/ApplicationRDSCluster.spec.ts @@ -0,0 +1,65 @@ +import { Testing } from 'cdktf'; +import { ApplicationRDSCluster } from './ApplicationRDSCluster'; + +describe('ApplicationRDSCluster', () => { + it('renders a RDS cluster', () => { + const synthed = Testing.synthScope((stack) => { + new ApplicationRDSCluster(stack, 'testRDSCluster', { + prefix: 'bowling-', + vpcId: 'rug', + subnetIds: ['0', '1'], + rdsConfig: { + masterUsername: 'walter', + masterPassword: 'bowling', + databaseName: 'walter', + engine: 'aurora-mysql', + }, + tags: { + whodis: 'walter', + }, + }); + }); + expect(synthed).toMatchSnapshot(); + }); + + it('renders a RDS cluster with a database URL', () => { + const synthed = Testing.synthScope((stack) => { + new ApplicationRDSCluster(stack, 'testRDSCluster', { + prefix: 'bowling-', + vpcId: 'rug', + subnetIds: ['0', '1'], + rdsConfig: { + masterUsername: 'walter', + masterPassword: 'bowling', + databaseName: 'walter', + engine: 'aurora-mysql', + }, + tags: { + whodis: 'walter', + }, + }); + }); + expect(synthed).toMatchSnapshot(); + }); + + it('renders a RDS cluster without a name', () => { + const synthed = Testing.synthScope((stack) => { + new ApplicationRDSCluster(stack, 'testRDSCluster', { + prefix: 'bowling-', + vpcId: 'rug', + subnetIds: ['0', '1'], + useName: false, + rdsConfig: { + masterUsername: 'walter', + masterPassword: 'bowling', + databaseName: 'walter', + engine: 'aurora-mysql', + }, + tags: { + whodis: 'walter', + }, + }); + }); + expect(synthed).toMatchSnapshot(); + }); +}); diff --git a/packages/terraform-modules/src/base/ApplicationRDSCluster.ts b/packages/terraform-modules/src/base/ApplicationRDSCluster.ts new file mode 100644 index 00000000..046cd653 --- /dev/null +++ b/packages/terraform-modules/src/base/ApplicationRDSCluster.ts @@ -0,0 +1,217 @@ +import { DataAwsVpc } from '@cdktf/provider-aws/lib/data-aws-vpc'; +import { DbSubnetGroup } from '@cdktf/provider-aws/lib/db-subnet-group'; +import { + RdsClusterConfig, + RdsCluster, +} from '@cdktf/provider-aws/lib/rds-cluster'; +import { SecretsmanagerSecret } from '@cdktf/provider-aws/lib/secretsmanager-secret'; +import { SecretsmanagerSecretVersion } from '@cdktf/provider-aws/lib/secretsmanager-secret-version'; +import { SecurityGroup } from '@cdktf/provider-aws/lib/security-group'; +import { TerraformMetaArguments, TerraformProvider } from 'cdktf'; +import { Construct } from 'constructs'; +import crypto from 'crypto'; + +//Override the default rds config but remove the items that we set ourselves. +export type ApplicationRDSClusterConfig = Omit< + RdsClusterConfig, + | 'clusterIdentifierPrefix' + | 'vpcSecurityGroupIds' + | 'dbSubnetGroupName' + | 'copyTagsToSnapshot' + | 'tags' + | 'lifecycle' +> & { + // masterUsername is not a required field in the RdsClusterConfig type but is required for create an RDS cluster + masterUsername: string; + masterPassword?: string; + engine?: 'aurora' | 'aurora-mysql' | 'aurora-postgresql'; +}; + +export interface ApplicationRDSClusterProps extends TerraformMetaArguments { + prefix: string; + vpcId: string; + subnetIds: string[]; + useName?: boolean; // When importing resources we need to tell terraform to not try and change the name of the resource. + rdsConfig: ApplicationRDSClusterConfig; + tags?: { [key: string]: string }; +} + +const defaults = { + useName: true, +}; + +/** + * Generates an RDS cluster + * + * The database will be initialized with a random password. + * + * If the database is Aurora or MySQL, a SecretsManager secret will be created with a rotation lambda + * that you can invoke in the AWS console after creation to rotate the password + */ +export class ApplicationRDSCluster extends Construct { + public readonly rds: RdsCluster; + public readonly secretARN?: string; + + constructor( + scope: Construct, + name: string, + config: ApplicationRDSClusterProps, + ) { + super(scope, name); + + // Apply defaults + config = { ...defaults, ...config }; + + const appVpc = new DataAwsVpc(this, `vpc`, { + filter: [ + { + name: 'vpc-id', + values: [config.vpcId], + }, + ], + provider: config.provider, + }); + + // Set the default port for mysql/postgresql based on the engine value for RDS + const rdsPort = config.rdsConfig.engine?.includes('postgresql') + ? 5432 + : 3306; + + const securityGroup = new SecurityGroup(this, 'rds_security_group', { + namePrefix: config.prefix, + description: 'Managed by Terraform', + vpcId: appVpc.id, + ingress: [ + { + fromPort: rdsPort, + toPort: rdsPort, + protocol: 'tcp', + cidrBlocks: [appVpc.cidrBlock], + // the following are included due to a bug + // https://github.com/hashicorp/terraform-cdk/issues/223 + description: null, + ipv6CidrBlocks: null, + prefixListIds: null, + securityGroups: null, + }, + ], + egress: [ + { + fromPort: 0, + protocol: '-1', + toPort: 0, + cidrBlocks: ['0.0.0.0/0'], + description: 'required', + ipv6CidrBlocks: [], + prefixListIds: [], + securityGroups: [], + }, + ], + provider: config.provider, + tags: config.tags, + }); + + const subnetGroup = new DbSubnetGroup(this, 'rds_subnet_group', { + namePrefix: config.useName ? config.prefix.toLowerCase() : undefined, + subnetIds: config.subnetIds, + provider: config.provider, + tags: config.tags, + }); + + this.rds = new RdsCluster(this, 'rds_cluster', { + ...config.rdsConfig, + clusterIdentifierPrefix: config.useName + ? config.prefix.toLowerCase() + : undefined, + tags: config.tags, + copyTagsToSnapshot: true, //Why would we ever want this to false?? + masterPassword: + config.rdsConfig.masterPassword ?? + crypto.randomBytes(8).toString('hex'), + vpcSecurityGroupIds: [securityGroup.id], + dbSubnetGroupName: subnetGroup.name, + lifecycle: { + ignoreChanges: ['master_username', 'master_password'], + }, + provider: config.provider, + }); + + // Create secrets manager resource for the RDS + // This value should be changed after initial creation + const { secretARN } = ApplicationRDSCluster.createRdsSecret( + this, + this.rds, + rdsPort, + config.prefix, + config.tags, + config.rdsConfig.engine, + config.provider, + ); + + this.secretARN = secretARN; + } + + /** + * Create an RDS secret + * + * @param scope + * @param rds + * @param rdsPort + * @param prefix + * @param tags + * @param engine + * @private + */ + private static createRdsSecret( + scope: Construct, + rds: RdsCluster, + rdsPort: number, + prefix: string, + tags?: { [key: string]: string }, + engine?: ApplicationRDSClusterConfig['engine'], + provider?: TerraformProvider, + ): { secretARN: string } { + //Create the secret + const secret = new SecretsmanagerSecret(scope, `rds_secret`, { + description: `Secret For ${rds.clusterIdentifier}`, + name: `${prefix}/${rds.clusterIdentifier}`, + //We dont auto rotate, because our apps dont have triggers to refresh yet. + //This is mainly so we can rotate after we create the + dependsOn: [rds], + provider, + tags, + }); + + const secretValues: { + engine: string; + host: string; + username: string; + password: string; + dbname: string; + port: number; + database_url?: string; + } = { + engine: engine, + host: rds.endpoint, + username: rds.masterUsername, + password: rds.masterPassword, + dbname: rds.databaseName, + port: rdsPort, + }; + + // Add a database URL to a MySQL-compatible Aurora instance + if (engine && engine === 'aurora-mysql') { + secretValues.database_url = `mysql://${rds.masterUsername}:${rds.masterPassword}@${rds.endpoint}:${rdsPort}/${rds.databaseName}`; + } + + //Create the initial secret version + new SecretsmanagerSecretVersion(scope, `rds_secret_version`, { + secretId: secret.id, + secretString: JSON.stringify(secretValues), + dependsOn: [secret], + provider, + }); + + return { secretARN: secret.arn }; + } +} diff --git a/packages/terraform-modules/src/base/ApplicationRedis.spec.ts b/packages/terraform-modules/src/base/ApplicationRedis.spec.ts new file mode 100644 index 00000000..a84ad71c --- /dev/null +++ b/packages/terraform-modules/src/base/ApplicationRedis.spec.ts @@ -0,0 +1,48 @@ +import { Testing } from 'cdktf'; +import { ApplicationElasticacheClusterProps } from './ApplicationElasticacheCluster'; + +import { ApplicationRedis } from './ApplicationRedis'; + +const BASE_CONFIG: ApplicationElasticacheClusterProps = { + allowedIngressSecurityGroupIds: [], + subnetIds: ['1234-123'], + vpcId: 'cool-vpc', + prefix: `abides-dev`, +}; + +describe('ApplicationRedis', () => { + it('renders redis with minimal config', () => { + const synthed = Testing.synthScope((stack) => { + new ApplicationRedis(stack, 'testRedis', BASE_CONFIG); + }); + expect(synthed).toMatchSnapshot(); + }); + + it('renders redis with tags', () => { + const config: ApplicationElasticacheClusterProps = { + ...BASE_CONFIG, + tags: { + letsgo: 'bowling', + donnie: 'throwinrockstonight', + }, + }; + const synthed = Testing.synthScope((stack) => { + new ApplicationRedis(stack, 'testRedis', config); + }); + expect(synthed).toMatchSnapshot(); + }); + + it('renders redis with node change', () => { + const config: ApplicationElasticacheClusterProps = { + ...BASE_CONFIG, + node: { + size: 'cache.m4.2xlarge', + count: 5, + }, + }; + const synthed = Testing.synthScope((stack) => { + new ApplicationRedis(stack, 'testRedis', config); + }); + expect(synthed).toMatchSnapshot(); + }); +}); diff --git a/packages/terraform-modules/src/base/ApplicationRedis.ts b/packages/terraform-modules/src/base/ApplicationRedis.ts new file mode 100644 index 00000000..428a19d0 --- /dev/null +++ b/packages/terraform-modules/src/base/ApplicationRedis.ts @@ -0,0 +1,96 @@ +import { + ApplicationElasticacheCluster, + ApplicationElasticacheClusterProps, + ApplicationElasticacheEngine, +} from './ApplicationElasticacheCluster'; +import { Construct } from 'constructs'; +import { DataAwsVpc } from '@cdktf/provider-aws/lib/data-aws-vpc'; +import { ElasticacheReplicationGroup } from '@cdktf/provider-aws/lib/elasticache-replication-group'; + +const DEFAULT_CONFIG = { + node: { + size: 'cache.t3.micro', + count: 2, + }, +}; + +export class ApplicationRedis extends ApplicationElasticacheCluster { + public elasticacheReplicationGroup: ElasticacheReplicationGroup; + + constructor( + scope: Construct, + name: string, + config: ApplicationElasticacheClusterProps, + ) { + super(scope, name); + + // use default config, but update any user-provided values + config = { + ...DEFAULT_CONFIG, + ...config, + }; + + const appVpc = ApplicationElasticacheCluster.getVpc(this, config); + + this.elasticacheReplicationGroup = + ApplicationRedis.createElasticacheReplicationCluster( + this, + appVpc, + config, + ); + } + + /** + * Creates the elasticache cluster to be used + * @param scope + * @param appVpc + * @param config + * @private + */ + private static createElasticacheReplicationCluster( + scope: Construct, + appVpc: DataAwsVpc, + config: ApplicationElasticacheClusterProps, + ): ElasticacheReplicationGroup { + const engine = ApplicationElasticacheEngine.REDIS; + const port = ApplicationElasticacheCluster.getPortForEngine(engine); + + const { securityGroup, subnetGroup } = + ApplicationRedis.createSecurityGroupAndSubnet( + scope, + config, + appVpc, + port, + ); + + return new ElasticacheReplicationGroup( + scope, + 'elasticache_replication_group', + { + replicationGroupId: `${config.prefix.toLowerCase()}`, + description: `${config.prefix.toLowerCase()} | Managed by terraform`, + nodeType: config.node.size, + port: port, + engineVersion: + config.overrideEngineVersion === null || + config.overrideEngineVersion === undefined + ? ApplicationRedis.getEngineVersionForEngine(engine) + : config.overrideEngineVersion, + parameterGroupName: + config.overrideParameterGroupName === null || + config.overrideParameterGroupName === undefined + ? ApplicationRedis.getParameterGroupForEngine(engine) + : config.overrideParameterGroupName, + automaticFailoverEnabled: true, + subnetGroupName: subnetGroup.name, + securityGroupIds: [securityGroup.id], + tags: config.tags, + applyImmediately: true, + dependsOn: [subnetGroup, securityGroup], + numCacheClusters: config.node.count ?? 2, + multiAzEnabled: true, + provider: config.provider, + }, + ); + } +} diff --git a/packages/terraform-modules/src/base/ApplicationSQSQueue.spec.ts b/packages/terraform-modules/src/base/ApplicationSQSQueue.spec.ts new file mode 100644 index 00000000..40ec2fea --- /dev/null +++ b/packages/terraform-modules/src/base/ApplicationSQSQueue.spec.ts @@ -0,0 +1,72 @@ +import { Testing, TerraformStack } from 'cdktf'; +import { + ApplicationSQSQueue, + ApplicationSQSQueueProps, +} from './ApplicationSQSQueue'; + +describe('ApplicationSQSQueue', () => { + it('renders an sqs queue without tags', () => { + const synthed = Testing.synthScope((stack) => { + new ApplicationSQSQueue(stack, 'testSQS', { + name: 'TEST', + }); + }); + expect(synthed).toMatchSnapshot(); + }); + + it('renders an sqs queue with tags', () => { + const synthed = Testing.synthScope((stack) => { + new ApplicationSQSQueue(stack, 'testSQS', { + name: 'TEST', + tags: { + test: '123', + environment: 'prod', + }, + }); + }); + expect(synthed).toMatchSnapshot(); + }); + + it('renders an sqs queue with max message size', () => { + const synthed = Testing.synthScope((stack) => { + new ApplicationSQSQueue(stack, 'testSQS', { + name: 'TEST', + maxMessageSize: 86753, + }); + }); + expect(synthed).toMatchSnapshot(); + }); + + const testFieldValidation = (config: ApplicationSQSQueueProps) => { + const sqsConfig = { + name: 'test', + ...config, + }; + + expect( + () => + new ApplicationSQSQueue( + new TerraformStack(Testing.app(), 'test'), + 'test-sqs', + sqsConfig, + ), + ).toThrow(Error); + }; + + it('test sqs validators', () => { + testFieldValidation({ visibilityTimeoutSeconds: -1 }); + testFieldValidation({ visibilityTimeoutSeconds: 43201 }); + + testFieldValidation({ messageRetentionSeconds: 59 }); + testFieldValidation({ messageRetentionSeconds: 1209601 }); + + testFieldValidation({ maxMessageSize: 234 }); + testFieldValidation({ maxMessageSize: 262145 }); + + testFieldValidation({ delaySeconds: -1 }); + testFieldValidation({ delaySeconds: 901 }); + + testFieldValidation({ receiveWaitTimeSeconds: -1 }); + testFieldValidation({ receiveWaitTimeSeconds: 24 }); + }); +}); diff --git a/packages/terraform-modules/src/base/ApplicationSQSQueue.ts b/packages/terraform-modules/src/base/ApplicationSQSQueue.ts new file mode 100644 index 00000000..c8bc568f --- /dev/null +++ b/packages/terraform-modules/src/base/ApplicationSQSQueue.ts @@ -0,0 +1,146 @@ +import { SqsQueue } from '@cdktf/provider-aws/lib/sqs-queue'; +import { TerraformMetaArguments } from 'cdktf'; +import { Construct } from 'constructs'; + +export interface ApplicationSQSQueueProps extends TerraformMetaArguments { + /** + * Name of the sqs queue + */ + name?: string; + /** + * The number of seconds Amazon SQS retains a message. + */ + messageRetentionSeconds?: number; + /** + * How many times a message can be received before it goes to a dead letter queue + * If not set or set to 0 this will instead max messages constantly retry + */ + maxReceiveCount?: number; + /** + * Max size the SQS message can be. In Bytes + */ + maxMessageSize?: number; + /** + * How long should a message wait before its allowed to be visible to a worker + */ + delaySeconds?: number; + /** + * Once a message is being worked on, how long can the worker hold it. + */ + visibilityTimeoutSeconds?: number; + /** + * How long should a worker be allowed to long poll + */ + receiveWaitTimeSeconds?: number; + tags?: { [key: string]: string }; +} + +/** + * Defines the validations that we need to perform against our configuration. + * These values are taken from https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/sqs_queue + */ +const validations: { + [key: string]: { + min: number; + max: number; + }; +} = { + visibilityTimeoutSeconds: { + min: 0, + max: 43200, + }, + messageRetentionSeconds: { + min: 60, + max: 1209600, + }, + maxMessageSize: { + min: 1024, + max: 262144, + }, + delaySeconds: { + min: 0, + max: 900, + }, + receiveWaitTimeSeconds: { + min: 0, + max: 20, + }, +}; + +/** + * Creates an SQS Queue with a dead letter queue + */ +export class ApplicationSQSQueue extends Construct { + public readonly sqsQueue: SqsQueue; + public deadLetterQueue: SqsQueue | undefined; + + constructor( + scope: Construct, + name: string, + config: ApplicationSQSQueueProps, + ) { + super(scope, name); + ApplicationSQSQueue.validateConfig(config); + this.sqsQueue = this.createSQSQueue(config); + } + + /** + * Validates the config against the values defined in: + * https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/sqs_queue + * @param config + * @private + */ + private static validateConfig(config: ApplicationSQSQueueProps): void { + for (const [key, valueToValidate] of Object.entries(config)) { + if (!(key in validations)) { + // The key value does not exist in the validations constant so no need to validate it + continue; + } + + const { min, max } = validations[key]; + if (valueToValidate < min || valueToValidate > max) { + throw new Error( + `${key} can not be greater then ${max} or less then ${min}`, + ); + } + } + } + + private createSQSQueue(config: ApplicationSQSQueueProps) { + //Have to use the any type because SqsQueueConfig marks the properties as readonly 🤦🏼‍ + const sqsConfig: any = { + name: config.name, + messageRetentionSeconds: config.messageRetentionSeconds, + maxMessageSize: config.maxMessageSize, + delaySeconds: config.delaySeconds, + visibilityTimeoutSeconds: config.visibilityTimeoutSeconds, + tags: config.tags, + fifoQueue: false, + provider: config.provider, + }; + + if (config.maxReceiveCount && config.maxReceiveCount > 0) { + this.deadLetterQueue = this.createDeadLetterSQSQueue(config); + sqsConfig.redrivePolicy = JSON.stringify({ + maxReceiveCount: config.maxReceiveCount, + deadLetterTargetArn: this.deadLetterQueue.arn, + }); + } + + return new SqsQueue(this, `sqs_queue`, sqsConfig); + } + + /** + * Create a dead letter queue + * @param config + * @private + */ + private createDeadLetterSQSQueue(config: ApplicationSQSQueueProps) { + return new SqsQueue(this, `redrive_sqs_queue`, { + name: `${config.name}-Deadletter`, + tags: config.tags, + fifoQueue: false, + provider: config.provider, + }); + } +} diff --git a/packages/terraform-modules/src/base/ApplicationSqsSnsTopicSubscription.spec.ts b/packages/terraform-modules/src/base/ApplicationSqsSnsTopicSubscription.spec.ts new file mode 100644 index 00000000..b906fd71 --- /dev/null +++ b/packages/terraform-modules/src/base/ApplicationSqsSnsTopicSubscription.spec.ts @@ -0,0 +1,55 @@ +import { SqsQueue } from '@cdktf/provider-aws/lib/sqs-queue'; +import { Testing } from 'cdktf'; +import { ApplicationSqsSnsTopicSubscription } from './ApplicationSqsSnsTopicSubscription'; + +describe('ApplicationSqsSnsTopicSubscription', () => { + const getConfig = (stack) => ({ + name: 'test-sns-subscription', + snsTopicArn: 'arn:aws:sns:TopicName', + sqsQueue: new SqsQueue(stack, 'sqs', { + name: 'test-sqs', + }), + }); + + const getConfigWithDlq = (stack) => ({ + name: 'test-sns-subscription', + snsTopicArn: 'arn:aws:sns:TopicName', + sqsQueue: new SqsQueue(stack, 'sqs', { + name: 'test-sqs', + }), + snsDlq: new SqsQueue(stack, 'dlq', { + name: 'test-sqs-dlq', + }), + }); + + it('renders an SQS SNS subscription without tags', () => { + const synthed = Testing.synthScope((stack) => { + new ApplicationSqsSnsTopicSubscription( + stack, + 'sqs-sns-subscription', + getConfig(stack), + ); + }); + expect(synthed).toMatchSnapshot(); + }); + + it('renders an SQS SNS subscription with tags', () => { + const synthed = Testing.synthScope((stack) => { + new ApplicationSqsSnsTopicSubscription(stack, 'sqs-sns-subscription', { + ...getConfig(stack), + tags: { hello: 'there' }, + }); + }); + expect(synthed).toMatchSnapshot(); + }); + + it('renders an SQS SNS subscription witg dlq passed', () => { + const synthed = Testing.synthScope((stack) => { + new ApplicationSqsSnsTopicSubscription(stack, 'sqs-sns-subscription', { + ...getConfigWithDlq(stack), + tags: { hello: 'there' }, + }); + }); + expect(synthed).toMatchSnapshot(); + }); +}); diff --git a/packages/terraform-modules/src/base/ApplicationSqsSnsTopicSubscription.ts b/packages/terraform-modules/src/base/ApplicationSqsSnsTopicSubscription.ts new file mode 100644 index 00000000..c649d85e --- /dev/null +++ b/packages/terraform-modules/src/base/ApplicationSqsSnsTopicSubscription.ts @@ -0,0 +1,122 @@ +import { DataAwsIamPolicyDocument } from '@cdktf/provider-aws/lib/data-aws-iam-policy-document'; +import { DataAwsSqsQueue } from '@cdktf/provider-aws/lib/data-aws-sqs-queue'; +import { + SnsTopicSubscription, + SnsTopicSubscriptionConfig, +} from '@cdktf/provider-aws/lib/sns-topic-subscription'; +import { SqsQueue } from '@cdktf/provider-aws/lib/sqs-queue'; +import { SqsQueuePolicy } from '@cdktf/provider-aws/lib/sqs-queue-policy'; +import { TerraformMetaArguments, TerraformResource } from 'cdktf'; +import { Construct } from 'constructs'; + +export interface ApplicationSqsSnsTopicSubscriptionProps + extends TerraformMetaArguments { + name: string; + snsTopicArn: string; + sqsQueue: SqsQueue | DataAwsSqsQueue; + snsDlq?: SqsQueue; + tags?: { [key: string]: string }; + dependsOn?: TerraformResource[]; +} + +/** + * Creates an SNS to SQS subscription + */ +export class ApplicationSqsSnsTopicSubscription extends Construct { + public readonly snsTopicSubscription: SnsTopicSubscription; + + constructor( + scope: Construct, + name: string, + private config: ApplicationSqsSnsTopicSubscriptionProps, + ) { + super(scope, name); + + const snsTopicDlq = this.config.snsDlq ?? this.createSqsSubscriptionDlq(); + this.snsTopicSubscription = this.createSnsTopicSubscription(snsTopicDlq); + this.createPoliciesForSnsToSQS(snsTopicDlq); + } + + /** + * Create a dead-letter queue for failed SNS messages + * @private + */ + private createSqsSubscriptionDlq(): SqsQueue { + return new SqsQueue(this, 'sns-topic-dql', { + name: `${this.config.name}-SNS-Topic-DLQ`, + tags: this.config.tags, + provider: this.config.provider, + }); + } + + /** + * Create an SNS subscription for SQS + * @param snsTopicDlq + * @private + */ + private createSnsTopicSubscription( + snsTopicDlq: SqsQueue, + ): SnsTopicSubscription { + return new SnsTopicSubscription(this, 'sns-subscription', { + topicArn: this.config.snsTopicArn, + protocol: 'sqs', + endpoint: this.config.sqsQueue.arn, + redrivePolicy: JSON.stringify({ + deadLetterTargetArn: snsTopicDlq.arn, + }), + dependsOn: [ + snsTopicDlq, + ...(this.config.dependsOn ? this.config.dependsOn : []), + ], + provider: this.config.provider, + } as SnsTopicSubscriptionConfig); + } + + /** + * Create IAM policies to allow SNS to write to the target SQS queue and a + * dead-letter queue + * @param snsTopicDlq + * @private + */ + private createPoliciesForSnsToSQS(snsTopicDlq: SqsQueue): void { + [ + { name: 'sns-sqs', resource: this.config.sqsQueue }, + { name: 'sns-dlq', resource: snsTopicDlq }, + ].forEach((queue) => { + const policy = new DataAwsIamPolicyDocument( + this, + `${queue.name}-policy-document`, + { + statement: [ + { + effect: 'Allow', + actions: ['sqs:SendMessage'], + resources: [queue.resource.arn], + principals: [ + { + identifiers: ['sns.amazonaws.com'], + type: 'Service', + }, + ], + condition: [ + { + test: 'ArnEquals', + variable: 'aws:SourceArn', + values: [this.config.snsTopicArn], + }, + ], + }, + ], + dependsOn: [queue.resource] as TerraformResource[], + provider: this.config.provider, + }, + ).json; + + new SqsQueuePolicy(this, `${queue.name}-policy`, { + queueUrl: queue.resource.url, + policy: policy, + provider: this.config.provider, + }); + }); + } +} diff --git a/packages/terraform-modules/src/base/ApplicationTargetGroup.spec.ts b/packages/terraform-modules/src/base/ApplicationTargetGroup.spec.ts new file mode 100644 index 00000000..0c692b62 --- /dev/null +++ b/packages/terraform-modules/src/base/ApplicationTargetGroup.spec.ts @@ -0,0 +1,30 @@ +import { Testing } from 'cdktf'; +import { ApplicationTargetGroup } from './ApplicationTargetGroup'; + +describe('ApplicationTargetGroup', () => { + it('renders a Target Group without tags', () => { + const synthed = Testing.synthScope((stack) => { + new ApplicationTargetGroup(stack, 'testTargetGroup', { + shortName: 'ABC', + vpcId: '123', + healthCheckPath: '/', + }); + }); + expect(synthed).toMatchSnapshot(); + }); + + it('renders a Target Group with tags', () => { + const synthed = Testing.synthScope((stack) => { + new ApplicationTargetGroup(stack, 'testTargetGroup', { + shortName: 'A1BC', + vpcId: '123', + healthCheckPath: '/', + tags: { + name: 'thedude', + hobby: 'bowling', + }, + }); + }); + expect(synthed).toMatchSnapshot(); + }); +}); diff --git a/packages/terraform-modules/src/base/ApplicationTargetGroup.ts b/packages/terraform-modules/src/base/ApplicationTargetGroup.ts new file mode 100644 index 00000000..d845e150 --- /dev/null +++ b/packages/terraform-modules/src/base/ApplicationTargetGroup.ts @@ -0,0 +1,40 @@ +import { AlbTargetGroup } from '@cdktf/provider-aws/lib/alb-target-group'; +import { TerraformMetaArguments } from 'cdktf'; +import { Construct } from 'constructs'; + +export interface ApplicationTargetGroupProps extends TerraformMetaArguments { + shortName: string; + vpcId: string; + healthCheckPath: string; + tags?: { [key: string]: string }; +} + +export class ApplicationTargetGroup extends Construct { + public readonly targetGroup: AlbTargetGroup; + + constructor( + scope: Construct, + name: string, + config: ApplicationTargetGroupProps, + ) { + super(scope, name); + + this.targetGroup = new AlbTargetGroup(this, 'ecs_target_group', { + namePrefix: config.shortName, + protocol: 'HTTP', + vpcId: config.vpcId, + tags: config.tags, + targetType: 'ip', + port: 80, + deregistrationDelay: '120', + healthCheck: { + interval: 15, + path: config.healthCheckPath, + protocol: 'HTTP', + healthyThreshold: 5, + unhealthyThreshold: 3, + }, + provider: config.provider, + }); + } +} diff --git a/packages/terraform-modules/src/base/ApplicationVersionedLambda.spec.ts b/packages/terraform-modules/src/base/ApplicationVersionedLambda.spec.ts new file mode 100644 index 00000000..75daaf79 --- /dev/null +++ b/packages/terraform-modules/src/base/ApplicationVersionedLambda.spec.ts @@ -0,0 +1,137 @@ +import { Testing } from 'cdktf'; +import { + ApplicationVersionedLambda, + ApplicationVersionedLambdaProps, + LAMBDA_RUNTIMES, +} from './ApplicationVersionedLambda'; + +const config: ApplicationVersionedLambdaProps = { + name: 'Test-Lambda', + runtime: LAMBDA_RUNTIMES.PYTHON38, + handler: 'index.handler', + s3Bucket: 'test-bucket', +}; + +describe('ApplicationVersionedLambda', () => { + it('renders a versioned lambda', () => { + const synthed = Testing.synthScope((stack) => { + new ApplicationVersionedLambda(stack, 'test-versioned-lambda', config); + }); + expect(synthed).toMatchSnapshot(); + }); + + it('renders a versioned lambda with tags', () => { + const synthed = Testing.synthScope((stack) => { + new ApplicationVersionedLambda(stack, 'test-versioned-lambda', { + ...config, + tags: { + my: 'tag', + for: 'test', + }, + }); + }); + expect(synthed).toMatchSnapshot(); + }); + + it('renders a versioned lambda with environment variables', () => { + const synthed = Testing.synthScope((stack) => { + new ApplicationVersionedLambda(stack, 'test-versioned-lambda', { + ...config, + environment: { + MY: 'env_var', + for: 'test', + }, + }); + }); + expect(synthed).toMatchSnapshot(); + }); + + it('renders a versioned lambda with timeout', () => { + const synthed = Testing.synthScope((stack) => { + new ApplicationVersionedLambda(stack, 'test-versioned-lambda', { + ...config, + timeout: 500, + }); + }); + expect(synthed).toMatchSnapshot(); + }); + + it('renders a versioned lambda with description', () => { + const synthed = Testing.synthScope((stack) => { + new ApplicationVersionedLambda(stack, 'test-versioned-lambda', { + ...config, + description: 'Test Description', + }); + }); + expect(synthed).toMatchSnapshot(); + }); + + it('renders a versioned lambda with execution policy statements', () => { + const synthed = Testing.synthScope((stack) => { + new ApplicationVersionedLambda(stack, 'test-versioned-lambda', { + ...config, + executionPolicyStatements: [ + { + effect: 'Allow', + actions: ['*'], + resources: ['*'], + }, + ], + }); + }); + expect(synthed).toMatchSnapshot(); + }); + + it('renders a versioned lambda with log retention', () => { + const synthed = Testing.synthScope((stack) => { + new ApplicationVersionedLambda(stack, 'test-versioned-lambda', { + ...config, + logRetention: 10, + }); + }); + expect(synthed).toMatchSnapshot(); + }); + + it('renders a versioned lambda with vpc', () => { + const synthed = Testing.synthScope((stack) => { + new ApplicationVersionedLambda(stack, 'test-versioned-lambda', { + ...config, + vpcConfig: { + subnetIds: ['1', '2'], + securityGroupIds: ['sec1', 'sec2'], + }, + }); + }); + expect(synthed).toMatchSnapshot(); + }); + + it('renders a versioned lambda with s3 bucket', () => { + const synthed = Testing.synthScope((stack) => { + new ApplicationVersionedLambda(stack, 'test-versioned-lambda', { + ...config, + s3Bucket: 'test-bucket', + }); + }); + expect(synthed).toMatchSnapshot(); + }); + + it('renders a versioned lambda with publish ignored', () => { + const synthed = Testing.synthScope((stack) => { + new ApplicationVersionedLambda(stack, 'test-versioned-lambda', { + ...config, + usesCodeDeploy: true, + }); + }); + expect(synthed).toMatchSnapshot(); + }); + + it('renders a lambda with a node v14 runtime', () => { + const synthed = Testing.synthScope((stack) => { + new ApplicationVersionedLambda(stack, 'test-versioned-lambda', { + ...config, + runtime: LAMBDA_RUNTIMES.NODEJS14, + }); + }); + expect(synthed).toMatchSnapshot(); + }); +}); diff --git a/packages/terraform-modules/src/base/ApplicationVersionedLambda.ts b/packages/terraform-modules/src/base/ApplicationVersionedLambda.ts new file mode 100644 index 00000000..e7653017 --- /dev/null +++ b/packages/terraform-modules/src/base/ApplicationVersionedLambda.ts @@ -0,0 +1,280 @@ +import { + DataArchiveFile, + DataArchiveFileSource, +} from '@cdktf/provider-archive/lib/data-archive-file'; +import { CloudwatchLogGroup } from '@cdktf/provider-aws/lib/cloudwatch-log-group'; +import { + DataAwsIamPolicyDocumentStatement, + DataAwsIamPolicyDocument, +} from '@cdktf/provider-aws/lib/data-aws-iam-policy-document'; +import { IamPolicy } from '@cdktf/provider-aws/lib/iam-policy'; +import { IamRole } from '@cdktf/provider-aws/lib/iam-role'; +import { IamRolePolicyAttachment } from '@cdktf/provider-aws/lib/iam-role-policy-attachment'; +import { LambdaAlias } from '@cdktf/provider-aws/lib/lambda-alias'; +import { + LambdaFunctionVpcConfig, + LambdaFunction, + LambdaFunctionConfig, +} from '@cdktf/provider-aws/lib/lambda-function'; +import { S3Bucket } from '@cdktf/provider-aws/lib/s3-bucket'; +import { S3BucketAcl } from '@cdktf/provider-aws/lib/s3-bucket-acl'; +import { S3BucketOwnershipControls } from '@cdktf/provider-aws/lib/s3-bucket-ownership-controls'; +import { S3BucketPublicAccessBlock } from '@cdktf/provider-aws/lib/s3-bucket-public-access-block'; +import { Fn, TerraformMetaArguments } from 'cdktf'; +import { Construct } from 'constructs'; + +export enum LAMBDA_RUNTIMES { + PYTHON38 = 'python3.8', + PYTHON39 = 'python3.9', + PYTHON310 = 'python3.10', + PYTHON311 = 'python3.11', + NODEJS14 = 'nodejs14.x', + NODEJS16 = 'nodejs16.x', + NODEJS18 = 'nodejs18.x', + NODEJS20 = 'nodejs20.x', +} + +export interface ApplicationVersionedLambdaProps + extends TerraformMetaArguments { + name: string; + description?: string; + runtime: LAMBDA_RUNTIMES; + handler: string; + timeout?: number; + reservedConcurrencyLimit?: number; + memorySizeInMb?: number; + environment?: { [key: string]: string }; + vpcConfig?: LambdaFunctionVpcConfig; + executionPolicyStatements?: DataAwsIamPolicyDocumentStatement[]; + tags?: { [key: string]: string }; + logRetention?: number; + s3Bucket: string; + usesCodeDeploy?: boolean; +} + +const DEFAULT_TIMEOUT = 5; +const DEFAULT_RETENTION = 14; +//https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/lambda_function#reserved_concurrent_executions +const DEFAULT_CONCURRENCY_LIMIT = -1; //unreserved concurrency +const DEFAULT_MEMORY_SIZE = 128; + +export class ApplicationVersionedLambda extends Construct { + public readonly versionedLambda: LambdaAlias; + public readonly defaultLambda: LambdaFunction; + public lambdaExecutionRole: IamRole; + + constructor( + scope: Construct, + name: string, + private config: ApplicationVersionedLambdaProps, + ) { + super(scope, name); + + this.createCodeBucket(); + const { versionedLambda, lambda } = this.createLambdaFunction(); + this.versionedLambda = versionedLambda; + this.defaultLambda = lambda; + } + + private createLambdaFunction() { + this.lambdaExecutionRole = new IamRole(this, 'execution-role', { + name: `${this.config.name}-ExecutionRole`, + assumeRolePolicy: this.getLambdaAssumePolicyDocument(), + provider: this.config.provider, + tags: this.config.tags, + }); + + const executionPolicy = new IamPolicy(this, 'execution-policy', { + name: `${this.config.name}-ExecutionRolePolicy`, + policy: this.getLambdaExecutionPolicyDocument(), + provider: this.config.provider, + tags: this.config.tags, + }); + + new IamRolePolicyAttachment(this, 'execution-role-policy-attachment', { + role: this.lambdaExecutionRole.name, + policyArn: executionPolicy.arn, + dependsOn: [this.lambdaExecutionRole, executionPolicy], + provider: this.config.provider, + }); + + const defaultLambda = this.getDefaultLambda(); + const lambdaConfig: LambdaFunctionConfig = { + functionName: `${this.config.name}-Function`, + filename: defaultLambda.outputPath, + handler: this.config.handler, + runtime: this.config.runtime, + timeout: this.config.timeout ?? DEFAULT_TIMEOUT, + sourceCodeHash: defaultLambda.outputBase64Sha256, + role: this.lambdaExecutionRole.arn, + memorySize: this.config.memorySizeInMb ?? DEFAULT_MEMORY_SIZE, + reservedConcurrentExecutions: + this.config.reservedConcurrencyLimit ?? DEFAULT_CONCURRENCY_LIMIT, + vpcConfig: this.config.vpcConfig, + publish: true, + lifecycle: { + ignoreChanges: [ + 'filename', + 'source_code_hash', + this.shouldIgnorePublish() ? 'publish' : '', + ].filter((v: string) => v), + }, + tags: this.config.tags, + environment: this.config.environment + ? { variables: this.config.environment } + : undefined, + provider: this.config.provider, + }; + + const lambda = new LambdaFunction(this, 'lambda', lambdaConfig); + + new CloudwatchLogGroup(this, 'log-group', { + name: `/aws/lambda/${lambda.functionName}`, + retentionInDays: this.config.logRetention ?? DEFAULT_RETENTION, + dependsOn: [lambda], + provider: this.config.provider, + tags: this.config.tags, + }); + + const versionedLambda = new LambdaAlias(this, 'alias', { + functionName: lambda.functionName, + functionVersion: Fn.element(Fn.split(':', lambda.qualifiedArn), 7), + name: 'DEPLOYED', + lifecycle: { + ignoreChanges: ['function_version'], + }, + dependsOn: [lambda], + provider: this.config.provider, + }); + return { versionedLambda, lambda }; + } + + private shouldIgnorePublish() { + if (this.config.usesCodeDeploy !== undefined) { + return this.config.usesCodeDeploy; + } + + return false; + } + + private getLambdaAssumePolicyDocument() { + return new DataAwsIamPolicyDocument(this, 'assume-policy-document', { + version: '2012-10-17', + statement: [ + { + effect: 'Allow', + actions: ['sts:AssumeRole'], + principals: [ + { + identifiers: ['lambda.amazonaws.com', 'edgelambda.amazonaws.com'], + type: 'Service', + }, + ], + }, + ], + provider: this.config.provider, + }).json; + } + + private getLambdaExecutionPolicyDocument() { + const document = { + version: '2012-10-17', + statement: [ + { + effect: 'Allow', + actions: [ + 'logs:CreateLogGroup', + 'logs:CreateLogStream', + 'logs:PutLogEvents', + 'logs:DescribeLogStreams', + ], + resources: ['arn:aws:logs:*:*:*'], + }, + ...(this.config.executionPolicyStatements ?? []), + ], + provider: this.config.provider, + }; + + if (this.config.vpcConfig) { + document.statement.push({ + effect: 'Allow', + actions: [ + 'ec2:DescribeNetworkInterfaces', + 'ec2:CreateNetworkInterface', + 'ec2:DeleteNetworkInterface', + 'ec2:DescribeInstances', + 'ec2:AttachNetworkInterface', + ], + resources: ['*'], + }); + } + + return new DataAwsIamPolicyDocument( + this, + 'execution-policy-document', + document, + ).json; + } + + private getDefaultLambda() { + const source = this.getDefaultLambdaSource(); + return new DataArchiveFile(this, 'lambda-default-file', { + type: 'zip', + source: [source], + outputPath: `${source.filename}.zip`, + }); + } + + private getDefaultLambdaSource(): DataArchiveFileSource { + const runtime = this.config.runtime.match(/[a-z]*/)[0]; + const handler = this.config.handler.split('.'); + const functionName = handler.pop(); + const functionFilename = handler.join('.'); + + let content = `exports.${functionName} = (event, context) => { console.log(event) }`; + let filename = `${functionFilename}.js`; + + if (runtime === 'python') { + content = `import json\ndef ${functionName}(event, context):\n\t print(event)\n\t return {'statusCode': 200, 'headers': {'dance': 'party'}, 'body': json.dumps({'electric': 'boogaloo'}), 'isBase64Encoded': False}`; + filename = `${functionFilename}.py`; + } + + return { + content, + filename, + }; + } + + private createCodeBucket() { + const codeBucket = new S3Bucket(this, 'code-bucket', { + bucket: this.config.s3Bucket, + tags: this.config.tags, + forceDestroy: true, + provider: this.config.provider, + }); + + const ownership = new S3BucketOwnershipControls( + this, + 'code-bucket-ownership-controls', + { + bucket: codeBucket.id, + rule: { + objectOwnership: 'BucketOwnerPreferred', + }, + }, + ); + + new S3BucketAcl(this, 'code-bucket-acl', { + bucket: codeBucket.id, + acl: 'private', + dependsOn: [ownership], + }); + + new S3BucketPublicAccessBlock(this, `code-bucket-public-access-block`, { + bucket: codeBucket.id, + blockPublicAcls: true, + blockPublicPolicy: true, + provider: this.config.provider, + }); + } +} diff --git a/packages/terraform-modules/src/base/__snapshots__/ApplicationAutoscaling.spec.ts.snap b/packages/terraform-modules/src/base/__snapshots__/ApplicationAutoscaling.spec.ts.snap new file mode 100644 index 00000000..17c39aa5 --- /dev/null +++ b/packages/terraform-modules/src/base/__snapshots__/ApplicationAutoscaling.spec.ts.snap @@ -0,0 +1,321 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ApplicationAutoscaling constructor renders autoscaling without tags 1`] = ` +"{ + "resource": { + "aws_appautoscaling_policy": { + "testAutoscaling_scale_in_policy_8085C07C": { + "depends_on": [ + "aws_appautoscaling_target.testAutoscaling_autoscaling_target_DFDF5955" + ], + "name": "test--ScaleInPolicy", + "policy_type": "StepScaling", + "resource_id": "\${aws_appautoscaling_target.testAutoscaling_autoscaling_target_DFDF5955.resource_id}", + "scalable_dimension": "\${aws_appautoscaling_target.testAutoscaling_autoscaling_target_DFDF5955.scalable_dimension}", + "service_namespace": "\${aws_appautoscaling_target.testAutoscaling_autoscaling_target_DFDF5955.service_namespace}", + "step_scaling_policy_configuration": { + "adjustment_type": "ChangeInCapacity", + "cooldown": 60, + "metric_aggregation_type": "Average", + "step_adjustment": [ + { + "metric_interval_upper_bound": "0", + "scaling_adjustment": -1 + } + ] + }, + "target_tracking_scaling_policy_configuration": null + }, + "testAutoscaling_scale_out_policy_DE69B04C": { + "depends_on": [ + "aws_appautoscaling_target.testAutoscaling_autoscaling_target_DFDF5955" + ], + "name": "test--ScaleOutPolicy", + "policy_type": "StepScaling", + "resource_id": "\${aws_appautoscaling_target.testAutoscaling_autoscaling_target_DFDF5955.resource_id}", + "scalable_dimension": "\${aws_appautoscaling_target.testAutoscaling_autoscaling_target_DFDF5955.scalable_dimension}", + "service_namespace": "\${aws_appautoscaling_target.testAutoscaling_autoscaling_target_DFDF5955.service_namespace}", + "step_scaling_policy_configuration": { + "adjustment_type": "ChangeInCapacity", + "cooldown": 60, + "metric_aggregation_type": "Average", + "step_adjustment": [ + { + "metric_interval_lower_bound": "0", + "scaling_adjustment": 2 + } + ] + }, + "target_tracking_scaling_policy_configuration": null + } + }, + "aws_appautoscaling_target": { + "testAutoscaling_autoscaling_target_DFDF5955": { + "max_capacity": 5, + "min_capacity": 1, + "resource_id": "service/ecs-cluster-test/ecs-service-test", + "scalable_dimension": "ecs:service:DesiredCount", + "service_namespace": "ecs" + } + }, + "aws_cloudwatch_metric_alarm": { + "testAutoscaling_scale_in_alarm_D552E48E": { + "alarm_actions": [ + "\${aws_appautoscaling_policy.testAutoscaling_scale_in_policy_8085C07C.arn}" + ], + "alarm_description": "Alarm to reduce capacity if container CPU is low", + "alarm_name": "test- Service Low CPU", + "comparison_operator": "LessThanThreshold", + "dimensions": { + "ClusterName": "ecs-cluster-test", + "ServiceName": "ecs-service-test" + }, + "evaluation_periods": 2, + "metric_name": "CPUUtilization", + "namespace": "AWS/ECS", + "period": 60, + "statistic": "Average", + "threshold": 30, + "treat_missing_data": "notBreaching" + }, + "testAutoscaling_scale_out_alarm_DBD72533": { + "alarm_actions": [ + "\${aws_appautoscaling_policy.testAutoscaling_scale_out_policy_DE69B04C.arn}" + ], + "alarm_description": "Alarm to add capacity if container CPU is high", + "alarm_name": "test- Service High CPU", + "comparison_operator": "GreaterThanThreshold", + "dimensions": { + "ClusterName": "ecs-cluster-test", + "ServiceName": "ecs-service-test" + }, + "evaluation_periods": 2, + "metric_name": "CPUUtilization", + "namespace": "AWS/ECS", + "period": 60, + "statistic": "Average", + "threshold": 45, + "treat_missing_data": "notBreaching" + } + } + } +}" +`; + +exports[`ApplicationAutoscaling generateAutoScalingPolicy renders a scale-in AutoscalingPolicy 1`] = ` +"{ + "resource": { + "aws_appautoscaling_policy": { + "test-resource_scale_in_policy_FC19C991": { + "depends_on": [ + "aws_appautoscaling_target.test-resource_autoscaling_target_50BC4A94" + ], + "name": "test--ScaleInPolicy", + "policy_type": "StepScaling", + "resource_id": "\${aws_appautoscaling_target.test-resource_autoscaling_target_50BC4A94.resource_id}", + "scalable_dimension": "\${aws_appautoscaling_target.test-resource_autoscaling_target_50BC4A94.scalable_dimension}", + "service_namespace": "\${aws_appautoscaling_target.test-resource_autoscaling_target_50BC4A94.service_namespace}", + "step_scaling_policy_configuration": { + "adjustment_type": "ChangeInCapacity", + "cooldown": 60, + "metric_aggregation_type": "Average", + "step_adjustment": [ + { + "metric_interval_upper_bound": "0", + "scaling_adjustment": -1 + } + ] + }, + "target_tracking_scaling_policy_configuration": null + } + }, + "aws_appautoscaling_target": { + "test-resource_autoscaling_target_50BC4A94": { + "max_capacity": 5, + "min_capacity": 1, + "resource_id": "service/ecs-cluster-test/ecs-service-test", + "scalable_dimension": "ecs:service:DesiredCount", + "service_namespace": "ecs" + } + } + } +}" +`; + +exports[`ApplicationAutoscaling generateAutoScalingPolicy renders a scale-out AutoscalingPolicy 1`] = ` +"{ + "resource": { + "aws_appautoscaling_policy": { + "test-resource_scale_out_policy_CE22053C": { + "depends_on": [ + "aws_appautoscaling_target.test-resource_autoscaling_target_50BC4A94" + ], + "name": "test--ScaleOutPolicy", + "policy_type": "StepScaling", + "resource_id": "\${aws_appautoscaling_target.test-resource_autoscaling_target_50BC4A94.resource_id}", + "scalable_dimension": "\${aws_appautoscaling_target.test-resource_autoscaling_target_50BC4A94.scalable_dimension}", + "service_namespace": "\${aws_appautoscaling_target.test-resource_autoscaling_target_50BC4A94.service_namespace}", + "step_scaling_policy_configuration": { + "adjustment_type": "ChangeInCapacity", + "cooldown": 60, + "metric_aggregation_type": "Average", + "step_adjustment": [ + { + "metric_interval_lower_bound": "0", + "scaling_adjustment": 2 + } + ] + }, + "target_tracking_scaling_policy_configuration": null + } + }, + "aws_appautoscaling_target": { + "test-resource_autoscaling_target_50BC4A94": { + "max_capacity": 5, + "min_capacity": 1, + "resource_id": "service/ecs-cluster-test/ecs-service-test", + "scalable_dimension": "ecs:service:DesiredCount", + "service_namespace": "ecs" + } + } + } +}" +`; + +exports[`ApplicationAutoscaling generateAutoScalingTarget renders an AutoscalingTarget 1`] = ` +"{ + "resource": { + "aws_appautoscaling_target": { + "test-resource_autoscaling_target_50BC4A94": { + "max_capacity": 5, + "min_capacity": 1, + "resource_id": "service/ecs-cluster-test/ecs-service-test", + "scalable_dimension": "ecs:service:DesiredCount", + "service_namespace": "ecs" + } + } + } +}" +`; + +exports[`ApplicationAutoscaling generateCloudwatchMetricAlarm renders a scale-in Cloudwatch Alarm 1`] = ` +"{ + "resource": { + "aws_appautoscaling_policy": { + "test-resource_scale_in_policy_FC19C991": { + "depends_on": [ + "aws_appautoscaling_target.test-resource_autoscaling_target_50BC4A94" + ], + "name": "test--ScaleInPolicy", + "policy_type": "StepScaling", + "resource_id": "\${aws_appautoscaling_target.test-resource_autoscaling_target_50BC4A94.resource_id}", + "scalable_dimension": "\${aws_appautoscaling_target.test-resource_autoscaling_target_50BC4A94.scalable_dimension}", + "service_namespace": "\${aws_appautoscaling_target.test-resource_autoscaling_target_50BC4A94.service_namespace}", + "step_scaling_policy_configuration": { + "adjustment_type": "ChangeInCapacity", + "cooldown": 60, + "metric_aggregation_type": "Average", + "step_adjustment": [ + { + "metric_interval_upper_bound": "0", + "scaling_adjustment": -1 + } + ] + }, + "target_tracking_scaling_policy_configuration": null + } + }, + "aws_appautoscaling_target": { + "test-resource_autoscaling_target_50BC4A94": { + "max_capacity": 5, + "min_capacity": 1, + "resource_id": "service/ecs-cluster-test/ecs-service-test", + "scalable_dimension": "ecs:service:DesiredCount", + "service_namespace": "ecs" + } + }, + "aws_cloudwatch_metric_alarm": { + "test-resource_scale_in_alarm_73254138": { + "alarm_actions": [ + "\${aws_appautoscaling_policy.test-resource_scale_in_policy_FC19C991.arn}" + ], + "alarm_description": "Alarm to reduce capacity if container CPU is low", + "alarm_name": "test- Service Low CPU", + "comparison_operator": "LessThanThreshold", + "dimensions": { + "ClusterName": "ecs-cluster-test", + "ServiceName": "ecs-service-test" + }, + "evaluation_periods": 2, + "metric_name": "CPUUtilization", + "namespace": "AWS/ECS", + "period": 60, + "statistic": "Average", + "threshold": 30, + "treat_missing_data": "notBreaching" + } + } + } +}" +`; + +exports[`ApplicationAutoscaling generateCloudwatchMetricAlarm renders a scale-out Cloudwatch Alarm 1`] = ` +"{ + "resource": { + "aws_appautoscaling_policy": { + "test-resource_scale_out_policy_CE22053C": { + "depends_on": [ + "aws_appautoscaling_target.test-resource_autoscaling_target_50BC4A94" + ], + "name": "test--ScaleOutPolicy", + "policy_type": "StepScaling", + "resource_id": "\${aws_appautoscaling_target.test-resource_autoscaling_target_50BC4A94.resource_id}", + "scalable_dimension": "\${aws_appautoscaling_target.test-resource_autoscaling_target_50BC4A94.scalable_dimension}", + "service_namespace": "\${aws_appautoscaling_target.test-resource_autoscaling_target_50BC4A94.service_namespace}", + "step_scaling_policy_configuration": { + "adjustment_type": "ChangeInCapacity", + "cooldown": 60, + "metric_aggregation_type": "Average", + "step_adjustment": [ + { + "metric_interval_lower_bound": "0", + "scaling_adjustment": 2 + } + ] + }, + "target_tracking_scaling_policy_configuration": null + } + }, + "aws_appautoscaling_target": { + "test-resource_autoscaling_target_50BC4A94": { + "max_capacity": 5, + "min_capacity": 1, + "resource_id": "service/ecs-cluster-test/ecs-service-test", + "scalable_dimension": "ecs:service:DesiredCount", + "service_namespace": "ecs" + } + }, + "aws_cloudwatch_metric_alarm": { + "test-resource_scale_out_alarm_5BADC0D8": { + "alarm_actions": [ + "\${aws_appautoscaling_policy.test-resource_scale_out_policy_CE22053C.arn}" + ], + "alarm_description": "Alarm to add capacity if container CPU is high", + "alarm_name": "test- Service High CPU", + "comparison_operator": "GreaterThanThreshold", + "dimensions": { + "ClusterName": "ecs-cluster-test", + "ServiceName": "ecs-service-test" + }, + "evaluation_periods": 2, + "metric_name": "CPUUtilization", + "namespace": "AWS/ECS", + "period": 60, + "statistic": "Average", + "threshold": 45, + "treat_missing_data": "notBreaching" + } + } + } +}" +`; diff --git a/packages/terraform-modules/src/base/__snapshots__/ApplicationBackups.spec.ts.snap b/packages/terraform-modules/src/base/__snapshots__/ApplicationBackups.spec.ts.snap new file mode 100644 index 00000000..ba8f795a --- /dev/null +++ b/packages/terraform-modules/src/base/__snapshots__/ApplicationBackups.spec.ts.snap @@ -0,0 +1,105 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ApplicationBackup renders vault with plans with tags 1`] = ` +"{ + "resource": { + "aws_backup_plan": { + "testBackup_backup-plan_AB394A83": { + "name": "TestPlan", + "rule": [ + { + "rule_name": "TestBackupRule", + "schedule": "cron( 0 5 ? * * *)", + "target_vault_name": "\${aws_backup_vault.testBackup_backup-vault_C40F1BAC.name}" + } + ], + "tags": { + "hobby": "bowling", + "name": "thedude" + } + } + }, + "aws_backup_selection": { + "testBackup_backup-selection_67969F7D": { + "iam_role_arn": "arn:aws:iam::1234567890:role/service-role/AWSBackupDefaultServiceRole", + "name": "prefix-Backup-Selection", + "plan_id": "\${aws_backup_plan.testBackup_backup-plan_AB394A83.id}", + "resources": [ + "arn:aws:rds:us-east-1:123456790:db:test" + ], + "selection_tag": [ + { + "key": "backups", + "type": "STRINGEQUALS", + "value": "True" + } + ] + } + }, + "aws_backup_vault": { + "testBackup_backup-vault_C40F1BAC": { + "kms_key_arn": "arn:aws:kms:us-east-1:1234567890:key/mrk-1234", + "name": "prefix-name", + "tags": { + "hobby": "bowling", + "name": "thedude" + } + } + }, + "aws_backup_vault_policy": { + "testBackup_backup-vault-policy_A278B298": { + "backup_vault_name": "\${aws_backup_vault.testBackup_backup-vault_C40F1BAC.name}", + "policy": "{\\"Version\\": \\"2012-10-17\\",\\"Statement\\": [{\\"Effect\\": \\"Allow\\",\\"Action\\": \\"backup:CopyIntoBackupVault\\",\\"Resource\\": \\"*\\",\\"Principal\\": \\"*\\",\\"Condition\\": {\\"StringEquals\\": {\\"aws:PrincipalOrgID\\": [\\"o-1234567890\\"]}}}]}" + } + } + } +}" +`; + +exports[`ApplicationBackup renders vault with plans without tags 1`] = ` +"{ + "resource": { + "aws_backup_plan": { + "testBackup_backup-plan_AB394A83": { + "name": "TestPlan", + "rule": [ + { + "rule_name": "TestBackupRule", + "schedule": "cron( 0 5 ? * * *)", + "target_vault_name": "\${aws_backup_vault.testBackup_backup-vault_C40F1BAC.name}" + } + ] + } + }, + "aws_backup_selection": { + "testBackup_backup-selection_67969F7D": { + "iam_role_arn": "arn:aws:iam::1234567890:role/service-role/AWSBackupDefaultServiceRole", + "name": "prefix-Backup-Selection", + "plan_id": "\${aws_backup_plan.testBackup_backup-plan_AB394A83.id}", + "resources": [ + "arn:aws:rds:us-east-1:123456790:db:test" + ], + "selection_tag": [ + { + "key": "backups", + "type": "STRINGEQUALS", + "value": "True" + } + ] + } + }, + "aws_backup_vault": { + "testBackup_backup-vault_C40F1BAC": { + "kms_key_arn": "arn:aws:kms:us-east-1:1234567890:key/mrk-1234", + "name": "prefix-name" + } + }, + "aws_backup_vault_policy": { + "testBackup_backup-vault-policy_A278B298": { + "backup_vault_name": "\${aws_backup_vault.testBackup_backup-vault_C40F1BAC.name}", + "policy": "{\\"Version\\": \\"2012-10-17\\",\\"Statement\\": [{\\"Effect\\": \\"Allow\\",\\"Action\\": \\"backup:CopyIntoBackupVault\\",\\"Resource\\": \\"*\\",\\"Principal\\": \\"*\\",\\"Condition\\": {\\"StringEquals\\": {\\"aws:PrincipalOrgID\\": [\\"o-1234567890\\"]}}}]}" + } + } + } +}" +`; diff --git a/packages/terraform-modules/src/base/__snapshots__/ApplicationBaseDNS.spec.ts.snap b/packages/terraform-modules/src/base/__snapshots__/ApplicationBaseDNS.spec.ts.snap new file mode 100644 index 00000000..e41f60d8 --- /dev/null +++ b/packages/terraform-modules/src/base/__snapshots__/ApplicationBaseDNS.spec.ts.snap @@ -0,0 +1,120 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ApplicationBaseDNS constructor renders base DNS with tags 1`] = ` +"{ + "data": { + "aws_route53_zone": { + "testDNS_main_hosted_zone": { + "name": "gobowling.info" + } + } + }, + "resource": { + "aws_route53_record": { + "testDNS_subhosted_zone_ns_5F02B4AB": { + "name": "dev.gobowling.info", + "records": "\${aws_route53_zone.testDNS_subhosted_zone_0D42EC63.name_servers}", + "ttl": 86400, + "type": "NS", + "zone_id": "\${data.aws_route53_zone.testDNS_main_hosted_zone.zone_id}" + } + }, + "aws_route53_zone": { + "testDNS_subhosted_zone_0D42EC63": { + "name": "dev.gobowling.info", + "tags": { + "hobby": "bowling", + "name": "thedude" + } + } + } + } +}" +`; + +exports[`ApplicationBaseDNS constructor renders base DNS without tags 1`] = ` +"{ + "data": { + "aws_route53_zone": { + "testDNS_main_hosted_zone": { + "name": "gobowling.info" + } + } + }, + "resource": { + "aws_route53_record": { + "testDNS_subhosted_zone_ns_5F02B4AB": { + "name": "dev.gobowling.info", + "records": "\${aws_route53_zone.testDNS_subhosted_zone_0D42EC63.name_servers}", + "ttl": 86400, + "type": "NS", + "zone_id": "\${data.aws_route53_zone.testDNS_main_hosted_zone.zone_id}" + } + }, + "aws_route53_zone": { + "testDNS_subhosted_zone_0D42EC63": { + "name": "dev.gobowling.info" + } + } + } +}" +`; + +exports[`ApplicationBaseDNS generateRoute53Record renders a route 53 record 1`] = ` +"{ + "resource": { + "aws_route53_record": { + "test-resource_subhosted_zone_ns_47594402": { + "name": "dev.gobowling.info", + "records": [ + "some", + "records" + ], + "ttl": 86400, + "type": "NS", + "zone_id": "some-zone-id" + } + } + } +}" +`; + +exports[`ApplicationBaseDNS generateRoute53Zone renders a route 53 zone 1`] = ` +"{ + "resource": { + "aws_route53_zone": { + "test-resource_subhosted_zone_262A1A23": { + "name": "dev.gobowling.info" + } + } + } +}" +`; + +exports[`ApplicationBaseDNS generateRoute53Zone renders a route 53 zone with tags 1`] = ` +"{ + "resource": { + "aws_route53_zone": { + "test-resource_subhosted_zone_262A1A23": { + "name": "dev.gobowling.info", + "tags": { + "hobby": "bowling", + "name": "thedude" + } + } + } + } +}" +`; + +exports[`ApplicationBaseDNS retrieveAwsRoute53Zone retrieves a route 53 zone 1`] = ` +"{ + "data": { + "aws_route53_zone": { + "test-resource_some-zone_main_hosted_zone_B55D697C": { + "name": "gobowling.info" + } + } + } +}" +`; diff --git a/packages/terraform-modules/src/base/__snapshots__/ApplicationCertificate.spec.ts.snap b/packages/terraform-modules/src/base/__snapshots__/ApplicationCertificate.spec.ts.snap new file mode 100644 index 00000000..9a1f30cf --- /dev/null +++ b/packages/terraform-modules/src/base/__snapshots__/ApplicationCertificate.spec.ts.snap @@ -0,0 +1,239 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ApplicationCertificate constructor renders a cert with a zone domain 1`] = ` +"{ + "resource": { + "aws_acm_certificate": { + "testCert_certificate_3EDC39B5": { + "domain_name": "dev.gobowling.info", + "lifecycle": { + "create_before_destroy": true + }, + "validation_method": "DNS" + } + }, + "aws_acm_certificate_validation": { + "testCert_certificate_validation_80E8BDA3": { + "certificate_arn": "\${aws_acm_certificate.testCert_certificate_3EDC39B5.arn}", + "depends_on": [ + "aws_route53_record.testCert_certificate_record_93AF392D", + "aws_acm_certificate.testCert_certificate_3EDC39B5" + ], + "validation_record_fqdns": [ + "\${aws_route53_record.testCert_certificate_record_93AF392D.fqdn}" + ] + } + }, + "aws_route53_record": { + "testCert_certificate_record_93AF392D": { + "depends_on": [ + "aws_acm_certificate.testCert_certificate_3EDC39B5" + ], + "name": "\${tolist(aws_acm_certificate.testCert_certificate_3EDC39B5.domain_validation_options).0.resource_record_name}", + "records": [ + "\${tolist(aws_acm_certificate.testCert_certificate_3EDC39B5.domain_validation_options).0.resource_record_value}" + ], + "ttl": 60, + "type": "\${tolist(aws_acm_certificate.testCert_certificate_3EDC39B5.domain_validation_options).0.resource_record_type}", + "zone_id": "gobowling.info" + } + } + } +}" +`; + +exports[`ApplicationCertificate constructor renders a cert with a zone id 1`] = ` +"{ + "resource": { + "aws_acm_certificate": { + "testCert_certificate_3EDC39B5": { + "domain_name": "dev.gobowling.info", + "lifecycle": { + "create_before_destroy": true + }, + "validation_method": "DNS" + } + }, + "aws_acm_certificate_validation": { + "testCert_certificate_validation_80E8BDA3": { + "certificate_arn": "\${aws_acm_certificate.testCert_certificate_3EDC39B5.arn}", + "depends_on": [ + "aws_route53_record.testCert_certificate_record_93AF392D", + "aws_acm_certificate.testCert_certificate_3EDC39B5" + ], + "validation_record_fqdns": [ + "\${aws_route53_record.testCert_certificate_record_93AF392D.fqdn}" + ] + } + }, + "aws_route53_record": { + "testCert_certificate_record_93AF392D": { + "depends_on": [ + "aws_acm_certificate.testCert_certificate_3EDC39B5" + ], + "name": "\${tolist(aws_acm_certificate.testCert_certificate_3EDC39B5.domain_validation_options).0.resource_record_name}", + "records": [ + "\${tolist(aws_acm_certificate.testCert_certificate_3EDC39B5.domain_validation_options).0.resource_record_value}" + ], + "ttl": 60, + "type": "\${tolist(aws_acm_certificate.testCert_certificate_3EDC39B5.domain_validation_options).0.resource_record_type}", + "zone_id": "malibu" + } + } + } +}" +`; + +exports[`ApplicationCertificate constructor renders a cert with tags 1`] = ` +"{ + "resource": { + "aws_acm_certificate": { + "testCert_certificate_3EDC39B5": { + "domain_name": "dev.gobowling.info", + "lifecycle": { + "create_before_destroy": true + }, + "tags": { + "hobby": "bowling", + "name": "thedude" + }, + "validation_method": "DNS" + } + }, + "aws_acm_certificate_validation": { + "testCert_certificate_validation_80E8BDA3": { + "certificate_arn": "\${aws_acm_certificate.testCert_certificate_3EDC39B5.arn}", + "depends_on": [ + "aws_route53_record.testCert_certificate_record_93AF392D", + "aws_acm_certificate.testCert_certificate_3EDC39B5" + ], + "validation_record_fqdns": [ + "\${aws_route53_record.testCert_certificate_record_93AF392D.fqdn}" + ] + } + }, + "aws_route53_record": { + "testCert_certificate_record_93AF392D": { + "depends_on": [ + "aws_acm_certificate.testCert_certificate_3EDC39B5" + ], + "name": "\${tolist(aws_acm_certificate.testCert_certificate_3EDC39B5.domain_validation_options).0.resource_record_name}", + "records": [ + "\${tolist(aws_acm_certificate.testCert_certificate_3EDC39B5.domain_validation_options).0.resource_record_value}" + ], + "ttl": 60, + "type": "\${tolist(aws_acm_certificate.testCert_certificate_3EDC39B5.domain_validation_options).0.resource_record_type}", + "zone_id": "gobowling.info" + } + } + } +}" +`; + +exports[`ApplicationCertificate generateAcmCertificate renders an acm certificate with tags 1`] = ` +"{ + "resource": { + "aws_acm_certificate": { + "test-resource_certificate_15680B70": { + "domain_name": "dev.gobowling.info", + "lifecycle": { + "create_before_destroy": true + }, + "tags": { + "hobby": "bowling", + "name": "thedude" + }, + "validation_method": "DNS" + } + } + } +}" +`; + +exports[`ApplicationCertificate generateAcmCertificate renders an acm certificate without tags 1`] = ` +"{ + "resource": { + "aws_acm_certificate": { + "test-resource_certificate_15680B70": { + "domain_name": "dev.gobowling.info", + "lifecycle": { + "create_before_destroy": true + }, + "validation_method": "DNS" + } + } + } +}" +`; + +exports[`ApplicationCertificate generateAcmCertificateValidation renders an acm certificate validation 1`] = ` +"{ + "resource": { + "aws_acm_certificate": { + "test-resource_certificate_15680B70": { + "domain_name": "dev.gobowling.info", + "lifecycle": { + "create_before_destroy": true + }, + "validation_method": "DNS" + } + }, + "aws_acm_certificate_validation": { + "test-resource_certificate_validation_7DB80AF0": { + "certificate_arn": "\${aws_acm_certificate.test-resource_certificate_15680B70.arn}", + "depends_on": [ + "aws_route53_record.test-resource_certificate_record_59EDF3C1", + "aws_acm_certificate.test-resource_certificate_15680B70" + ], + "validation_record_fqdns": [ + "\${aws_route53_record.test-resource_certificate_record_59EDF3C1.fqdn}" + ] + } + }, + "aws_route53_record": { + "test-resource_certificate_record_59EDF3C1": { + "depends_on": [ + "aws_acm_certificate.test-resource_certificate_15680B70" + ], + "name": "\${tolist(aws_acm_certificate.test-resource_certificate_15680B70.domain_validation_options).0.resource_record_name}", + "records": [ + "\${tolist(aws_acm_certificate.test-resource_certificate_15680B70.domain_validation_options).0.resource_record_value}" + ], + "ttl": 60, + "type": "\${tolist(aws_acm_certificate.test-resource_certificate_15680B70.domain_validation_options).0.resource_record_type}", + "zone_id": "dev.gobowling.info" + } + } + } +}" +`; + +exports[`ApplicationCertificate generateRoute53Record renders a route 53 record 1`] = ` +"{ + "resource": { + "aws_acm_certificate": { + "test-resource_certificate_15680B70": { + "domain_name": "dev.gobowling.info", + "lifecycle": { + "create_before_destroy": true + }, + "validation_method": "DNS" + } + }, + "aws_route53_record": { + "test-resource_certificate_record_59EDF3C1": { + "depends_on": [ + "aws_acm_certificate.test-resource_certificate_15680B70" + ], + "name": "\${tolist(aws_acm_certificate.test-resource_certificate_15680B70.domain_validation_options).0.resource_record_name}", + "records": [ + "\${tolist(aws_acm_certificate.test-resource_certificate_15680B70.domain_validation_options).0.resource_record_value}" + ], + "ttl": 60, + "type": "\${tolist(aws_acm_certificate.test-resource_certificate_15680B70.domain_validation_options).0.resource_record_type}", + "zone_id": "dev.gobowling.info" + } + } + } +}" +`; diff --git a/packages/terraform-modules/src/base/__snapshots__/ApplicationDynamoDBTable.spec.ts.snap b/packages/terraform-modules/src/base/__snapshots__/ApplicationDynamoDBTable.spec.ts.snap new file mode 100644 index 00000000..e3b94af4 --- /dev/null +++ b/packages/terraform-modules/src/base/__snapshots__/ApplicationDynamoDBTable.spec.ts.snap @@ -0,0 +1,1388 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ApplicationDynamoDBTable renders a dynamodb table with streams enabled 1`] = ` +"{ + "resource": { + "aws_dynamodb_table": { + "testDynamoDBTable_dynamodb_table_A3DD49A4": { + "billing_mode": "PROVISIONED", + "lifecycle": { + "ignore_changes": [ + "read_capacity", + "write_capacity" + ], + "prevent_destroy": true + }, + "name": "abides-", + "stream_enabled": true, + "stream_view_type": "NEW_AND_OLD_IMAGES" + } + } + } +}" +`; + +exports[`ApplicationDynamoDBTable renders dynamo db table global secondary indexes 1`] = ` +"{ + "data": { + "aws_iam_policy_document": { + "testDynamoDBTable_ReadCapacity_assume_role_policy_document_54F698F0": { + "statement": [ + { + "actions": [ + "sts:AssumeRole" + ], + "effect": "Allow", + "principals": [ + { + "identifiers": [ + "application-autoscaling.amazonaws.com" + ], + "type": "Service" + } + ] + } + ] + }, + "testDynamoDBTable_ReadCapacity_policy_document_EF789C68": { + "statement": [ + { + "actions": [ + "application-autoscaling:*", + "cloudwatch:DescribeAlarms", + "cloudwatch:PutMetricAlarm" + ], + "effect": "Allow", + "resources": [ + "*" + ] + }, + { + "actions": [ + "dynamodb:DescribeTable", + "dynamodb:UpdateTable" + ], + "effect": "Allow", + "resources": [ + "\${aws_dynamodb_table.testDynamoDBTable_dynamodb_table_A3DD49A4.arn}", + "\${aws_dynamodb_table.testDynamoDBTable_dynamodb_table_A3DD49A4.arn}*" + ] + } + ] + }, + "testDynamoDBTable_WriteCapacity_assume_role_policy_document_5A7EA325": { + "statement": [ + { + "actions": [ + "sts:AssumeRole" + ], + "effect": "Allow", + "principals": [ + { + "identifiers": [ + "application-autoscaling.amazonaws.com" + ], + "type": "Service" + } + ] + } + ] + }, + "testDynamoDBTable_WriteCapacity_policy_document_9032D023": { + "statement": [ + { + "actions": [ + "application-autoscaling:*", + "cloudwatch:DescribeAlarms", + "cloudwatch:PutMetricAlarm" + ], + "effect": "Allow", + "resources": [ + "*" + ] + }, + { + "actions": [ + "dynamodb:DescribeTable", + "dynamodb:UpdateTable" + ], + "effect": "Allow", + "resources": [ + "\${aws_dynamodb_table.testDynamoDBTable_dynamodb_table_A3DD49A4.arn}", + "\${aws_dynamodb_table.testDynamoDBTable_dynamodb_table_A3DD49A4.arn}*" + ] + } + ] + } + } + }, + "resource": { + "aws_appautoscaling_policy": { + "testDynamoDBTable_card-index_ReadCapacity_index_policy_81408390": { + "depends_on": [ + "aws_appautoscaling_target.testDynamoDBTable_card-index_ReadCapacity_index_target_8A2F409F", + "aws_dynamodb_table.testDynamoDBTable_dynamodb_table_A3DD49A4" + ], + "name": "DynamoDBReadCapacityUtilization:\${aws_appautoscaling_target.testDynamoDBTable_card-index_ReadCapacity_index_target_8A2F409F.resource_id}", + "policy_type": "TargetTrackingScaling", + "resource_id": "\${aws_appautoscaling_target.testDynamoDBTable_card-index_ReadCapacity_index_target_8A2F409F.resource_id}", + "scalable_dimension": "\${aws_appautoscaling_target.testDynamoDBTable_card-index_ReadCapacity_index_target_8A2F409F.scalable_dimension}", + "service_namespace": "\${aws_appautoscaling_target.testDynamoDBTable_card-index_ReadCapacity_index_target_8A2F409F.service_namespace}", + "target_tracking_scaling_policy_configuration": { + "predefined_metric_specification": { + "predefined_metric_type": "DynamoDBReadCapacityUtilization" + }, + "target_value": 1 + } + }, + "testDynamoDBTable_card-index_WriteCapacity_index_policy_C3071E31": { + "depends_on": [ + "aws_appautoscaling_target.testDynamoDBTable_card-index_WriteCapacity_index_target_F5D86C4A", + "aws_dynamodb_table.testDynamoDBTable_dynamodb_table_A3DD49A4" + ], + "name": "DynamoDBWriteCapacityUtilization:\${aws_appautoscaling_target.testDynamoDBTable_card-index_WriteCapacity_index_target_F5D86C4A.resource_id}", + "policy_type": "TargetTrackingScaling", + "resource_id": "\${aws_appautoscaling_target.testDynamoDBTable_card-index_WriteCapacity_index_target_F5D86C4A.resource_id}", + "scalable_dimension": "\${aws_appautoscaling_target.testDynamoDBTable_card-index_WriteCapacity_index_target_F5D86C4A.scalable_dimension}", + "service_namespace": "\${aws_appautoscaling_target.testDynamoDBTable_card-index_WriteCapacity_index_target_F5D86C4A.service_namespace}", + "target_tracking_scaling_policy_configuration": { + "predefined_metric_specification": { + "predefined_metric_type": "DynamoDBWriteCapacityUtilization" + }, + "target_value": 1 + } + }, + "testDynamoDBTable_testDynamoDBTable_dynamodb_table_A3DD49A4_ReadCapacity_table_policy_558BE4C0": { + "depends_on": [ + "aws_appautoscaling_target.testDynamoDBTable_testDynamoDBTable_dynamodb_table_A3DD49A4_ReadCapacity_table_target_5A95550B", + "aws_dynamodb_table.testDynamoDBTable_dynamodb_table_A3DD49A4" + ], + "name": "DynamoDBReadCapacityUtilization:\${aws_appautoscaling_target.testDynamoDBTable_testDynamoDBTable_dynamodb_table_A3DD49A4_ReadCapacity_table_target_5A95550B.resource_id}", + "policy_type": "TargetTrackingScaling", + "resource_id": "\${aws_appautoscaling_target.testDynamoDBTable_testDynamoDBTable_dynamodb_table_A3DD49A4_ReadCapacity_table_target_5A95550B.resource_id}", + "scalable_dimension": "\${aws_appautoscaling_target.testDynamoDBTable_testDynamoDBTable_dynamodb_table_A3DD49A4_ReadCapacity_table_target_5A95550B.scalable_dimension}", + "service_namespace": "\${aws_appautoscaling_target.testDynamoDBTable_testDynamoDBTable_dynamodb_table_A3DD49A4_ReadCapacity_table_target_5A95550B.service_namespace}", + "target_tracking_scaling_policy_configuration": { + "predefined_metric_specification": { + "predefined_metric_type": "DynamoDBReadCapacityUtilization" + }, + "target_value": 1 + } + }, + "testDynamoDBTable_testDynamoDBTable_dynamodb_table_A3DD49A4_WriteCapacity_table_policy_45352666": { + "depends_on": [ + "aws_appautoscaling_target.testDynamoDBTable_testDynamoDBTable_dynamodb_table_A3DD49A4_WriteCapacity_table_target_EC052FEE", + "aws_dynamodb_table.testDynamoDBTable_dynamodb_table_A3DD49A4" + ], + "name": "DynamoDBWriteCapacityUtilization:\${aws_appautoscaling_target.testDynamoDBTable_testDynamoDBTable_dynamodb_table_A3DD49A4_WriteCapacity_table_target_EC052FEE.resource_id}", + "policy_type": "TargetTrackingScaling", + "resource_id": "\${aws_appautoscaling_target.testDynamoDBTable_testDynamoDBTable_dynamodb_table_A3DD49A4_WriteCapacity_table_target_EC052FEE.resource_id}", + "scalable_dimension": "\${aws_appautoscaling_target.testDynamoDBTable_testDynamoDBTable_dynamodb_table_A3DD49A4_WriteCapacity_table_target_EC052FEE.scalable_dimension}", + "service_namespace": "\${aws_appautoscaling_target.testDynamoDBTable_testDynamoDBTable_dynamodb_table_A3DD49A4_WriteCapacity_table_target_EC052FEE.service_namespace}", + "target_tracking_scaling_policy_configuration": { + "predefined_metric_specification": { + "predefined_metric_type": "DynamoDBWriteCapacityUtilization" + }, + "target_value": 1 + } + } + }, + "aws_appautoscaling_target": { + "testDynamoDBTable_card-index_ReadCapacity_index_target_8A2F409F": { + "depends_on": [ + "aws_dynamodb_table.testDynamoDBTable_dynamodb_table_A3DD49A4" + ], + "max_capacity": 10, + "min_capacity": 5, + "resource_id": "table/\${aws_dynamodb_table.testDynamoDBTable_dynamodb_table_A3DD49A4.name}/index/card-index", + "role_arn": "\${aws_iam_role.testDynamoDBTable_ReadCapacity_role_2B9645BB.arn}", + "scalable_dimension": "dynamodb:index:ReadCapacityUnits", + "service_namespace": "dynamodb" + }, + "testDynamoDBTable_card-index_WriteCapacity_index_target_F5D86C4A": { + "depends_on": [ + "aws_dynamodb_table.testDynamoDBTable_dynamodb_table_A3DD49A4" + ], + "max_capacity": 10, + "min_capacity": 5, + "resource_id": "table/\${aws_dynamodb_table.testDynamoDBTable_dynamodb_table_A3DD49A4.name}/index/card-index", + "role_arn": "\${aws_iam_role.testDynamoDBTable_WriteCapacity_role_4BB8E3F1.arn}", + "scalable_dimension": "dynamodb:index:WriteCapacityUnits", + "service_namespace": "dynamodb" + }, + "testDynamoDBTable_testDynamoDBTable_dynamodb_table_A3DD49A4_ReadCapacity_table_target_5A95550B": { + "depends_on": [ + "aws_dynamodb_table.testDynamoDBTable_dynamodb_table_A3DD49A4" + ], + "max_capacity": 10, + "min_capacity": 3, + "resource_id": "table/\${aws_dynamodb_table.testDynamoDBTable_dynamodb_table_A3DD49A4.name}", + "role_arn": "\${aws_iam_role.testDynamoDBTable_ReadCapacity_role_2B9645BB.arn}", + "scalable_dimension": "dynamodb:table:ReadCapacityUnits", + "service_namespace": "dynamodb" + }, + "testDynamoDBTable_testDynamoDBTable_dynamodb_table_A3DD49A4_WriteCapacity_table_target_EC052FEE": { + "depends_on": [ + "aws_dynamodb_table.testDynamoDBTable_dynamodb_table_A3DD49A4" + ], + "max_capacity": 10, + "min_capacity": 3, + "resource_id": "table/\${aws_dynamodb_table.testDynamoDBTable_dynamodb_table_A3DD49A4.name}", + "role_arn": "\${aws_iam_role.testDynamoDBTable_WriteCapacity_role_4BB8E3F1.arn}", + "scalable_dimension": "dynamodb:table:WriteCapacityUnits", + "service_namespace": "dynamodb" + } + }, + "aws_dynamodb_table": { + "testDynamoDBTable_dynamodb_table_A3DD49A4": { + "attribute": [ + { + "name": "attribeautiful", + "type": "shrugs!" + } + ], + "billing_mode": "PROVISIONED", + "global_secondary_index": [ + { + "hash_key": "card-type", + "name": "card-index", + "projection_type": "ALL", + "range_key": "home_on_the_range", + "read_capacity": 5, + "write_capacity": 5 + } + ], + "hash_key": "123", + "lifecycle": { + "ignore_changes": [ + "read_capacity", + "write_capacity" + ], + "prevent_destroy": true + }, + "name": "abides-" + } + }, + "aws_iam_policy": { + "testDynamoDBTable_ReadCapacity_autoscaling_policy_52DA6649": { + "name": "abides--ReadCapacity-AutoScalingPolicy", + "policy": "\${data.aws_iam_policy_document.testDynamoDBTable_ReadCapacity_policy_document_EF789C68.json}" + }, + "testDynamoDBTable_WriteCapacity_autoscaling_policy_9632CE60": { + "name": "abides--WriteCapacity-AutoScalingPolicy", + "policy": "\${data.aws_iam_policy_document.testDynamoDBTable_WriteCapacity_policy_document_9032D023.json}" + } + }, + "aws_iam_role": { + "testDynamoDBTable_ReadCapacity_role_2B9645BB": { + "assume_role_policy": "\${data.aws_iam_policy_document.testDynamoDBTable_ReadCapacity_assume_role_policy_document_54F698F0.json}", + "name": "abides--ReadCapacity-AutoScalingRole" + }, + "testDynamoDBTable_WriteCapacity_role_4BB8E3F1": { + "assume_role_policy": "\${data.aws_iam_policy_document.testDynamoDBTable_WriteCapacity_assume_role_policy_document_5A7EA325.json}", + "name": "abides--WriteCapacity-AutoScalingRole" + } + }, + "aws_iam_role_policy_attachment": { + "testDynamoDBTable_ReadCapacity_role_attachment_3C0829C2": { + "depends_on": [ + "aws_iam_role.testDynamoDBTable_ReadCapacity_role_2B9645BB", + "aws_iam_policy.testDynamoDBTable_ReadCapacity_autoscaling_policy_52DA6649" + ], + "policy_arn": "\${aws_iam_policy.testDynamoDBTable_ReadCapacity_autoscaling_policy_52DA6649.arn}", + "role": "\${aws_iam_role.testDynamoDBTable_ReadCapacity_role_2B9645BB.name}" + }, + "testDynamoDBTable_WriteCapacity_role_attachment_8ABB9833": { + "depends_on": [ + "aws_iam_role.testDynamoDBTable_WriteCapacity_role_4BB8E3F1", + "aws_iam_policy.testDynamoDBTable_WriteCapacity_autoscaling_policy_9632CE60" + ], + "policy_arn": "\${aws_iam_policy.testDynamoDBTable_WriteCapacity_autoscaling_policy_9632CE60.arn}", + "role": "\${aws_iam_role.testDynamoDBTable_WriteCapacity_role_4BB8E3F1.name}" + } + } + } +}" +`; + +exports[`ApplicationDynamoDBTable renders dynamo db table that is not protected from being destroyed 1`] = ` +"{ + "resource": { + "aws_dynamodb_table": { + "testDynamoDBTable_dynamodb_table_A3DD49A4": { + "attribute": [ + { + "name": "attribeautiful", + "type": "shrugs!" + } + ], + "billing_mode": "PROVISIONED", + "global_secondary_index": [ + ], + "hash_key": "123", + "lifecycle": { + "ignore_changes": [ + "read_capacity", + "write_capacity" + ], + "prevent_destroy": false + }, + "name": "abides-" + } + } + } +}" +`; + +exports[`ApplicationDynamoDBTable renders dynamo db table with 2 global secondary indexes 1`] = ` +"{ + "data": { + "aws_iam_policy_document": { + "testDynamoDBTable_ReadCapacity_assume_role_policy_document_54F698F0": { + "statement": [ + { + "actions": [ + "sts:AssumeRole" + ], + "effect": "Allow", + "principals": [ + { + "identifiers": [ + "application-autoscaling.amazonaws.com" + ], + "type": "Service" + } + ] + } + ] + }, + "testDynamoDBTable_ReadCapacity_policy_document_EF789C68": { + "statement": [ + { + "actions": [ + "application-autoscaling:*", + "cloudwatch:DescribeAlarms", + "cloudwatch:PutMetricAlarm" + ], + "effect": "Allow", + "resources": [ + "*" + ] + }, + { + "actions": [ + "dynamodb:DescribeTable", + "dynamodb:UpdateTable" + ], + "effect": "Allow", + "resources": [ + "\${aws_dynamodb_table.testDynamoDBTable_dynamodb_table_A3DD49A4.arn}", + "\${aws_dynamodb_table.testDynamoDBTable_dynamodb_table_A3DD49A4.arn}*" + ] + } + ] + }, + "testDynamoDBTable_WriteCapacity_assume_role_policy_document_5A7EA325": { + "statement": [ + { + "actions": [ + "sts:AssumeRole" + ], + "effect": "Allow", + "principals": [ + { + "identifiers": [ + "application-autoscaling.amazonaws.com" + ], + "type": "Service" + } + ] + } + ] + }, + "testDynamoDBTable_WriteCapacity_policy_document_9032D023": { + "statement": [ + { + "actions": [ + "application-autoscaling:*", + "cloudwatch:DescribeAlarms", + "cloudwatch:PutMetricAlarm" + ], + "effect": "Allow", + "resources": [ + "*" + ] + }, + { + "actions": [ + "dynamodb:DescribeTable", + "dynamodb:UpdateTable" + ], + "effect": "Allow", + "resources": [ + "\${aws_dynamodb_table.testDynamoDBTable_dynamodb_table_A3DD49A4.arn}", + "\${aws_dynamodb_table.testDynamoDBTable_dynamodb_table_A3DD49A4.arn}*" + ] + } + ] + } + } + }, + "resource": { + "aws_appautoscaling_policy": { + "testDynamoDBTable_card-index-2_ReadCapacity_index_policy_72866E91": { + "depends_on": [ + "aws_appautoscaling_target.testDynamoDBTable_card-index-2_ReadCapacity_index_target_5B34D257", + "aws_dynamodb_table.testDynamoDBTable_dynamodb_table_A3DD49A4" + ], + "name": "DynamoDBReadCapacityUtilization:\${aws_appautoscaling_target.testDynamoDBTable_card-index-2_ReadCapacity_index_target_5B34D257.resource_id}", + "policy_type": "TargetTrackingScaling", + "resource_id": "\${aws_appautoscaling_target.testDynamoDBTable_card-index-2_ReadCapacity_index_target_5B34D257.resource_id}", + "scalable_dimension": "\${aws_appautoscaling_target.testDynamoDBTable_card-index-2_ReadCapacity_index_target_5B34D257.scalable_dimension}", + "service_namespace": "\${aws_appautoscaling_target.testDynamoDBTable_card-index-2_ReadCapacity_index_target_5B34D257.service_namespace}", + "target_tracking_scaling_policy_configuration": { + "predefined_metric_specification": { + "predefined_metric_type": "DynamoDBReadCapacityUtilization" + }, + "target_value": 1 + } + }, + "testDynamoDBTable_card-index-2_WriteCapacity_index_policy_BDD340E7": { + "depends_on": [ + "aws_appautoscaling_target.testDynamoDBTable_card-index-2_WriteCapacity_index_target_2BCC5603", + "aws_dynamodb_table.testDynamoDBTable_dynamodb_table_A3DD49A4" + ], + "name": "DynamoDBWriteCapacityUtilization:\${aws_appautoscaling_target.testDynamoDBTable_card-index-2_WriteCapacity_index_target_2BCC5603.resource_id}", + "policy_type": "TargetTrackingScaling", + "resource_id": "\${aws_appautoscaling_target.testDynamoDBTable_card-index-2_WriteCapacity_index_target_2BCC5603.resource_id}", + "scalable_dimension": "\${aws_appautoscaling_target.testDynamoDBTable_card-index-2_WriteCapacity_index_target_2BCC5603.scalable_dimension}", + "service_namespace": "\${aws_appautoscaling_target.testDynamoDBTable_card-index-2_WriteCapacity_index_target_2BCC5603.service_namespace}", + "target_tracking_scaling_policy_configuration": { + "predefined_metric_specification": { + "predefined_metric_type": "DynamoDBWriteCapacityUtilization" + }, + "target_value": 1 + } + }, + "testDynamoDBTable_testDynamoDBTable_dynamodb_table_A3DD49A4_ReadCapacity_table_policy_558BE4C0": { + "depends_on": [ + "aws_appautoscaling_target.testDynamoDBTable_testDynamoDBTable_dynamodb_table_A3DD49A4_ReadCapacity_table_target_5A95550B", + "aws_dynamodb_table.testDynamoDBTable_dynamodb_table_A3DD49A4" + ], + "name": "DynamoDBReadCapacityUtilization:\${aws_appautoscaling_target.testDynamoDBTable_testDynamoDBTable_dynamodb_table_A3DD49A4_ReadCapacity_table_target_5A95550B.resource_id}", + "policy_type": "TargetTrackingScaling", + "resource_id": "\${aws_appautoscaling_target.testDynamoDBTable_testDynamoDBTable_dynamodb_table_A3DD49A4_ReadCapacity_table_target_5A95550B.resource_id}", + "scalable_dimension": "\${aws_appautoscaling_target.testDynamoDBTable_testDynamoDBTable_dynamodb_table_A3DD49A4_ReadCapacity_table_target_5A95550B.scalable_dimension}", + "service_namespace": "\${aws_appautoscaling_target.testDynamoDBTable_testDynamoDBTable_dynamodb_table_A3DD49A4_ReadCapacity_table_target_5A95550B.service_namespace}", + "target_tracking_scaling_policy_configuration": { + "predefined_metric_specification": { + "predefined_metric_type": "DynamoDBReadCapacityUtilization" + }, + "target_value": 1 + } + }, + "testDynamoDBTable_testDynamoDBTable_dynamodb_table_A3DD49A4_WriteCapacity_table_policy_45352666": { + "depends_on": [ + "aws_appautoscaling_target.testDynamoDBTable_testDynamoDBTable_dynamodb_table_A3DD49A4_WriteCapacity_table_target_EC052FEE", + "aws_dynamodb_table.testDynamoDBTable_dynamodb_table_A3DD49A4" + ], + "name": "DynamoDBWriteCapacityUtilization:\${aws_appautoscaling_target.testDynamoDBTable_testDynamoDBTable_dynamodb_table_A3DD49A4_WriteCapacity_table_target_EC052FEE.resource_id}", + "policy_type": "TargetTrackingScaling", + "resource_id": "\${aws_appautoscaling_target.testDynamoDBTable_testDynamoDBTable_dynamodb_table_A3DD49A4_WriteCapacity_table_target_EC052FEE.resource_id}", + "scalable_dimension": "\${aws_appautoscaling_target.testDynamoDBTable_testDynamoDBTable_dynamodb_table_A3DD49A4_WriteCapacity_table_target_EC052FEE.scalable_dimension}", + "service_namespace": "\${aws_appautoscaling_target.testDynamoDBTable_testDynamoDBTable_dynamodb_table_A3DD49A4_WriteCapacity_table_target_EC052FEE.service_namespace}", + "target_tracking_scaling_policy_configuration": { + "predefined_metric_specification": { + "predefined_metric_type": "DynamoDBWriteCapacityUtilization" + }, + "target_value": 1 + } + } + }, + "aws_appautoscaling_target": { + "testDynamoDBTable_card-index-2_ReadCapacity_index_target_5B34D257": { + "depends_on": [ + "aws_dynamodb_table.testDynamoDBTable_dynamodb_table_A3DD49A4" + ], + "max_capacity": 10, + "min_capacity": 10, + "resource_id": "table/\${aws_dynamodb_table.testDynamoDBTable_dynamodb_table_A3DD49A4.name}/index/card-index-2", + "role_arn": "\${aws_iam_role.testDynamoDBTable_ReadCapacity_role_2B9645BB.arn}", + "scalable_dimension": "dynamodb:index:ReadCapacityUnits", + "service_namespace": "dynamodb" + }, + "testDynamoDBTable_card-index-2_WriteCapacity_index_target_2BCC5603": { + "depends_on": [ + "aws_dynamodb_table.testDynamoDBTable_dynamodb_table_A3DD49A4" + ], + "max_capacity": 10, + "min_capacity": 10, + "resource_id": "table/\${aws_dynamodb_table.testDynamoDBTable_dynamodb_table_A3DD49A4.name}/index/card-index-2", + "role_arn": "\${aws_iam_role.testDynamoDBTable_WriteCapacity_role_4BB8E3F1.arn}", + "scalable_dimension": "dynamodb:index:WriteCapacityUnits", + "service_namespace": "dynamodb" + }, + "testDynamoDBTable_testDynamoDBTable_dynamodb_table_A3DD49A4_ReadCapacity_table_target_5A95550B": { + "depends_on": [ + "aws_dynamodb_table.testDynamoDBTable_dynamodb_table_A3DD49A4" + ], + "max_capacity": 10, + "min_capacity": 3, + "resource_id": "table/\${aws_dynamodb_table.testDynamoDBTable_dynamodb_table_A3DD49A4.name}", + "role_arn": "\${aws_iam_role.testDynamoDBTable_ReadCapacity_role_2B9645BB.arn}", + "scalable_dimension": "dynamodb:table:ReadCapacityUnits", + "service_namespace": "dynamodb" + }, + "testDynamoDBTable_testDynamoDBTable_dynamodb_table_A3DD49A4_WriteCapacity_table_target_EC052FEE": { + "depends_on": [ + "aws_dynamodb_table.testDynamoDBTable_dynamodb_table_A3DD49A4" + ], + "max_capacity": 10, + "min_capacity": 3, + "resource_id": "table/\${aws_dynamodb_table.testDynamoDBTable_dynamodb_table_A3DD49A4.name}", + "role_arn": "\${aws_iam_role.testDynamoDBTable_WriteCapacity_role_4BB8E3F1.arn}", + "scalable_dimension": "dynamodb:table:WriteCapacityUnits", + "service_namespace": "dynamodb" + } + }, + "aws_dynamodb_table": { + "testDynamoDBTable_dynamodb_table_A3DD49A4": { + "attribute": [ + { + "name": "attribeautiful", + "type": "shrugs!" + } + ], + "billing_mode": "PROVISIONED", + "global_secondary_index": [ + { + "hash_key": "card-type-123", + "name": "card-index-2", + "projection_type": "ALL", + "range_key": "home_home_on_the_range", + "read_capacity": 10, + "write_capacity": 10 + } + ], + "hash_key": "123", + "lifecycle": { + "ignore_changes": [ + "read_capacity", + "write_capacity" + ], + "prevent_destroy": true + }, + "name": "abides-" + } + }, + "aws_iam_policy": { + "testDynamoDBTable_ReadCapacity_autoscaling_policy_52DA6649": { + "name": "abides--ReadCapacity-AutoScalingPolicy", + "policy": "\${data.aws_iam_policy_document.testDynamoDBTable_ReadCapacity_policy_document_EF789C68.json}" + }, + "testDynamoDBTable_WriteCapacity_autoscaling_policy_9632CE60": { + "name": "abides--WriteCapacity-AutoScalingPolicy", + "policy": "\${data.aws_iam_policy_document.testDynamoDBTable_WriteCapacity_policy_document_9032D023.json}" + } + }, + "aws_iam_role": { + "testDynamoDBTable_ReadCapacity_role_2B9645BB": { + "assume_role_policy": "\${data.aws_iam_policy_document.testDynamoDBTable_ReadCapacity_assume_role_policy_document_54F698F0.json}", + "name": "abides--ReadCapacity-AutoScalingRole" + }, + "testDynamoDBTable_WriteCapacity_role_4BB8E3F1": { + "assume_role_policy": "\${data.aws_iam_policy_document.testDynamoDBTable_WriteCapacity_assume_role_policy_document_5A7EA325.json}", + "name": "abides--WriteCapacity-AutoScalingRole" + } + }, + "aws_iam_role_policy_attachment": { + "testDynamoDBTable_ReadCapacity_role_attachment_3C0829C2": { + "depends_on": [ + "aws_iam_role.testDynamoDBTable_ReadCapacity_role_2B9645BB", + "aws_iam_policy.testDynamoDBTable_ReadCapacity_autoscaling_policy_52DA6649" + ], + "policy_arn": "\${aws_iam_policy.testDynamoDBTable_ReadCapacity_autoscaling_policy_52DA6649.arn}", + "role": "\${aws_iam_role.testDynamoDBTable_ReadCapacity_role_2B9645BB.name}" + }, + "testDynamoDBTable_WriteCapacity_role_attachment_8ABB9833": { + "depends_on": [ + "aws_iam_role.testDynamoDBTable_WriteCapacity_role_4BB8E3F1", + "aws_iam_policy.testDynamoDBTable_WriteCapacity_autoscaling_policy_9632CE60" + ], + "policy_arn": "\${aws_iam_policy.testDynamoDBTable_WriteCapacity_autoscaling_policy_9632CE60.arn}", + "role": "\${aws_iam_role.testDynamoDBTable_WriteCapacity_role_4BB8E3F1.name}" + } + } + } +}" +`; + +exports[`ApplicationDynamoDBTable renders dynamo db table with minimal config 1`] = ` +"{ + "resource": { + "aws_dynamodb_table": { + "testDynamoDBTable_dynamodb_table_A3DD49A4": { + "attribute": [ + { + "name": "attribeautiful", + "type": "shrugs!" + } + ], + "billing_mode": "PROVISIONED", + "global_secondary_index": [ + ], + "hash_key": "123", + "lifecycle": { + "ignore_changes": [ + "read_capacity", + "write_capacity" + ], + "prevent_destroy": true + }, + "name": "abides-" + } + } + } +}" +`; + +exports[`ApplicationDynamoDBTable renders dynamo db table with on-demand capacity 1`] = ` +"{ + "resource": { + "aws_dynamodb_table": { + "testDynamoDBTable_dynamodb_table_A3DD49A4": { + "attribute": [ + { + "name": "attribeautiful", + "type": "shrugs!" + } + ], + "billing_mode": "PAY_PER_REQUEST", + "global_secondary_index": [ + ], + "hash_key": "123", + "lifecycle": { + "ignore_changes": [ + "read_capacity", + "write_capacity" + ], + "prevent_destroy": true + }, + "name": "abides-" + } + } + } +}" +`; + +exports[`ApplicationDynamoDBTable renders dynamo db table with read and write capacity 1`] = ` +"{ + "data": { + "aws_iam_policy_document": { + "testDynamoDBTable_ReadCapacity_assume_role_policy_document_54F698F0": { + "statement": [ + { + "actions": [ + "sts:AssumeRole" + ], + "effect": "Allow", + "principals": [ + { + "identifiers": [ + "application-autoscaling.amazonaws.com" + ], + "type": "Service" + } + ] + } + ] + }, + "testDynamoDBTable_ReadCapacity_policy_document_EF789C68": { + "statement": [ + { + "actions": [ + "application-autoscaling:*", + "cloudwatch:DescribeAlarms", + "cloudwatch:PutMetricAlarm" + ], + "effect": "Allow", + "resources": [ + "*" + ] + }, + { + "actions": [ + "dynamodb:DescribeTable", + "dynamodb:UpdateTable" + ], + "effect": "Allow", + "resources": [ + "\${aws_dynamodb_table.testDynamoDBTable_dynamodb_table_A3DD49A4.arn}", + "\${aws_dynamodb_table.testDynamoDBTable_dynamodb_table_A3DD49A4.arn}*" + ] + } + ] + }, + "testDynamoDBTable_WriteCapacity_assume_role_policy_document_5A7EA325": { + "statement": [ + { + "actions": [ + "sts:AssumeRole" + ], + "effect": "Allow", + "principals": [ + { + "identifiers": [ + "application-autoscaling.amazonaws.com" + ], + "type": "Service" + } + ] + } + ] + }, + "testDynamoDBTable_WriteCapacity_policy_document_9032D023": { + "statement": [ + { + "actions": [ + "application-autoscaling:*", + "cloudwatch:DescribeAlarms", + "cloudwatch:PutMetricAlarm" + ], + "effect": "Allow", + "resources": [ + "*" + ] + }, + { + "actions": [ + "dynamodb:DescribeTable", + "dynamodb:UpdateTable" + ], + "effect": "Allow", + "resources": [ + "\${aws_dynamodb_table.testDynamoDBTable_dynamodb_table_A3DD49A4.arn}", + "\${aws_dynamodb_table.testDynamoDBTable_dynamodb_table_A3DD49A4.arn}*" + ] + } + ] + } + } + }, + "resource": { + "aws_appautoscaling_policy": { + "testDynamoDBTable_testDynamoDBTable_dynamodb_table_A3DD49A4_ReadCapacity_table_policy_558BE4C0": { + "depends_on": [ + "aws_appautoscaling_target.testDynamoDBTable_testDynamoDBTable_dynamodb_table_A3DD49A4_ReadCapacity_table_target_5A95550B", + "aws_dynamodb_table.testDynamoDBTable_dynamodb_table_A3DD49A4" + ], + "name": "DynamoDBReadCapacityUtilization:\${aws_appautoscaling_target.testDynamoDBTable_testDynamoDBTable_dynamodb_table_A3DD49A4_ReadCapacity_table_target_5A95550B.resource_id}", + "policy_type": "TargetTrackingScaling", + "resource_id": "\${aws_appautoscaling_target.testDynamoDBTable_testDynamoDBTable_dynamodb_table_A3DD49A4_ReadCapacity_table_target_5A95550B.resource_id}", + "scalable_dimension": "\${aws_appautoscaling_target.testDynamoDBTable_testDynamoDBTable_dynamodb_table_A3DD49A4_ReadCapacity_table_target_5A95550B.scalable_dimension}", + "service_namespace": "\${aws_appautoscaling_target.testDynamoDBTable_testDynamoDBTable_dynamodb_table_A3DD49A4_ReadCapacity_table_target_5A95550B.service_namespace}", + "target_tracking_scaling_policy_configuration": { + "predefined_metric_specification": { + "predefined_metric_type": "DynamoDBReadCapacityUtilization" + }, + "target_value": 1 + } + }, + "testDynamoDBTable_testDynamoDBTable_dynamodb_table_A3DD49A4_WriteCapacity_table_policy_45352666": { + "depends_on": [ + "aws_appautoscaling_target.testDynamoDBTable_testDynamoDBTable_dynamodb_table_A3DD49A4_WriteCapacity_table_target_EC052FEE", + "aws_dynamodb_table.testDynamoDBTable_dynamodb_table_A3DD49A4" + ], + "name": "DynamoDBWriteCapacityUtilization:\${aws_appautoscaling_target.testDynamoDBTable_testDynamoDBTable_dynamodb_table_A3DD49A4_WriteCapacity_table_target_EC052FEE.resource_id}", + "policy_type": "TargetTrackingScaling", + "resource_id": "\${aws_appautoscaling_target.testDynamoDBTable_testDynamoDBTable_dynamodb_table_A3DD49A4_WriteCapacity_table_target_EC052FEE.resource_id}", + "scalable_dimension": "\${aws_appautoscaling_target.testDynamoDBTable_testDynamoDBTable_dynamodb_table_A3DD49A4_WriteCapacity_table_target_EC052FEE.scalable_dimension}", + "service_namespace": "\${aws_appautoscaling_target.testDynamoDBTable_testDynamoDBTable_dynamodb_table_A3DD49A4_WriteCapacity_table_target_EC052FEE.service_namespace}", + "target_tracking_scaling_policy_configuration": { + "predefined_metric_specification": { + "predefined_metric_type": "DynamoDBWriteCapacityUtilization" + }, + "target_value": 1 + } + } + }, + "aws_appautoscaling_target": { + "testDynamoDBTable_testDynamoDBTable_dynamodb_table_A3DD49A4_ReadCapacity_table_target_5A95550B": { + "depends_on": [ + "aws_dynamodb_table.testDynamoDBTable_dynamodb_table_A3DD49A4" + ], + "max_capacity": 10, + "min_capacity": 3, + "resource_id": "table/\${aws_dynamodb_table.testDynamoDBTable_dynamodb_table_A3DD49A4.name}", + "role_arn": "\${aws_iam_role.testDynamoDBTable_ReadCapacity_role_2B9645BB.arn}", + "scalable_dimension": "dynamodb:table:ReadCapacityUnits", + "service_namespace": "dynamodb" + }, + "testDynamoDBTable_testDynamoDBTable_dynamodb_table_A3DD49A4_WriteCapacity_table_target_EC052FEE": { + "depends_on": [ + "aws_dynamodb_table.testDynamoDBTable_dynamodb_table_A3DD49A4" + ], + "max_capacity": 10, + "min_capacity": 3, + "resource_id": "table/\${aws_dynamodb_table.testDynamoDBTable_dynamodb_table_A3DD49A4.name}", + "role_arn": "\${aws_iam_role.testDynamoDBTable_WriteCapacity_role_4BB8E3F1.arn}", + "scalable_dimension": "dynamodb:table:WriteCapacityUnits", + "service_namespace": "dynamodb" + } + }, + "aws_dynamodb_table": { + "testDynamoDBTable_dynamodb_table_A3DD49A4": { + "attribute": [ + { + "name": "attribeautiful", + "type": "shrugs!" + } + ], + "billing_mode": "PROVISIONED", + "global_secondary_index": [ + ], + "hash_key": "123", + "lifecycle": { + "ignore_changes": [ + "read_capacity", + "write_capacity" + ], + "prevent_destroy": true + }, + "name": "abides-" + } + }, + "aws_iam_policy": { + "testDynamoDBTable_ReadCapacity_autoscaling_policy_52DA6649": { + "name": "abides--ReadCapacity-AutoScalingPolicy", + "policy": "\${data.aws_iam_policy_document.testDynamoDBTable_ReadCapacity_policy_document_EF789C68.json}" + }, + "testDynamoDBTable_WriteCapacity_autoscaling_policy_9632CE60": { + "name": "abides--WriteCapacity-AutoScalingPolicy", + "policy": "\${data.aws_iam_policy_document.testDynamoDBTable_WriteCapacity_policy_document_9032D023.json}" + } + }, + "aws_iam_role": { + "testDynamoDBTable_ReadCapacity_role_2B9645BB": { + "assume_role_policy": "\${data.aws_iam_policy_document.testDynamoDBTable_ReadCapacity_assume_role_policy_document_54F698F0.json}", + "name": "abides--ReadCapacity-AutoScalingRole" + }, + "testDynamoDBTable_WriteCapacity_role_4BB8E3F1": { + "assume_role_policy": "\${data.aws_iam_policy_document.testDynamoDBTable_WriteCapacity_assume_role_policy_document_5A7EA325.json}", + "name": "abides--WriteCapacity-AutoScalingRole" + } + }, + "aws_iam_role_policy_attachment": { + "testDynamoDBTable_ReadCapacity_role_attachment_3C0829C2": { + "depends_on": [ + "aws_iam_role.testDynamoDBTable_ReadCapacity_role_2B9645BB", + "aws_iam_policy.testDynamoDBTable_ReadCapacity_autoscaling_policy_52DA6649" + ], + "policy_arn": "\${aws_iam_policy.testDynamoDBTable_ReadCapacity_autoscaling_policy_52DA6649.arn}", + "role": "\${aws_iam_role.testDynamoDBTable_ReadCapacity_role_2B9645BB.name}" + }, + "testDynamoDBTable_WriteCapacity_role_attachment_8ABB9833": { + "depends_on": [ + "aws_iam_role.testDynamoDBTable_WriteCapacity_role_4BB8E3F1", + "aws_iam_policy.testDynamoDBTable_WriteCapacity_autoscaling_policy_9632CE60" + ], + "policy_arn": "\${aws_iam_policy.testDynamoDBTable_WriteCapacity_autoscaling_policy_9632CE60.arn}", + "role": "\${aws_iam_role.testDynamoDBTable_WriteCapacity_role_4BB8E3F1.name}" + } + } + } +}" +`; + +exports[`ApplicationDynamoDBTable renders dynamo db table with read and write capacity and tags 1`] = ` +"{ + "data": { + "aws_iam_policy_document": { + "testDynamoDBTable_ReadCapacity_assume_role_policy_document_54F698F0": { + "statement": [ + { + "actions": [ + "sts:AssumeRole" + ], + "effect": "Allow", + "principals": [ + { + "identifiers": [ + "application-autoscaling.amazonaws.com" + ], + "type": "Service" + } + ] + } + ] + }, + "testDynamoDBTable_ReadCapacity_policy_document_EF789C68": { + "statement": [ + { + "actions": [ + "application-autoscaling:*", + "cloudwatch:DescribeAlarms", + "cloudwatch:PutMetricAlarm" + ], + "effect": "Allow", + "resources": [ + "*" + ] + }, + { + "actions": [ + "dynamodb:DescribeTable", + "dynamodb:UpdateTable" + ], + "effect": "Allow", + "resources": [ + "\${aws_dynamodb_table.testDynamoDBTable_dynamodb_table_A3DD49A4.arn}", + "\${aws_dynamodb_table.testDynamoDBTable_dynamodb_table_A3DD49A4.arn}*" + ] + } + ] + }, + "testDynamoDBTable_WriteCapacity_assume_role_policy_document_5A7EA325": { + "statement": [ + { + "actions": [ + "sts:AssumeRole" + ], + "effect": "Allow", + "principals": [ + { + "identifiers": [ + "application-autoscaling.amazonaws.com" + ], + "type": "Service" + } + ] + } + ] + }, + "testDynamoDBTable_WriteCapacity_policy_document_9032D023": { + "statement": [ + { + "actions": [ + "application-autoscaling:*", + "cloudwatch:DescribeAlarms", + "cloudwatch:PutMetricAlarm" + ], + "effect": "Allow", + "resources": [ + "*" + ] + }, + { + "actions": [ + "dynamodb:DescribeTable", + "dynamodb:UpdateTable" + ], + "effect": "Allow", + "resources": [ + "\${aws_dynamodb_table.testDynamoDBTable_dynamodb_table_A3DD49A4.arn}", + "\${aws_dynamodb_table.testDynamoDBTable_dynamodb_table_A3DD49A4.arn}*" + ] + } + ] + } + } + }, + "resource": { + "aws_appautoscaling_policy": { + "testDynamoDBTable_testDynamoDBTable_dynamodb_table_A3DD49A4_ReadCapacity_table_policy_558BE4C0": { + "depends_on": [ + "aws_appautoscaling_target.testDynamoDBTable_testDynamoDBTable_dynamodb_table_A3DD49A4_ReadCapacity_table_target_5A95550B", + "aws_dynamodb_table.testDynamoDBTable_dynamodb_table_A3DD49A4" + ], + "name": "DynamoDBReadCapacityUtilization:\${aws_appautoscaling_target.testDynamoDBTable_testDynamoDBTable_dynamodb_table_A3DD49A4_ReadCapacity_table_target_5A95550B.resource_id}", + "policy_type": "TargetTrackingScaling", + "resource_id": "\${aws_appautoscaling_target.testDynamoDBTable_testDynamoDBTable_dynamodb_table_A3DD49A4_ReadCapacity_table_target_5A95550B.resource_id}", + "scalable_dimension": "\${aws_appautoscaling_target.testDynamoDBTable_testDynamoDBTable_dynamodb_table_A3DD49A4_ReadCapacity_table_target_5A95550B.scalable_dimension}", + "service_namespace": "\${aws_appautoscaling_target.testDynamoDBTable_testDynamoDBTable_dynamodb_table_A3DD49A4_ReadCapacity_table_target_5A95550B.service_namespace}", + "target_tracking_scaling_policy_configuration": { + "predefined_metric_specification": { + "predefined_metric_type": "DynamoDBReadCapacityUtilization" + }, + "target_value": 1 + } + }, + "testDynamoDBTable_testDynamoDBTable_dynamodb_table_A3DD49A4_WriteCapacity_table_policy_45352666": { + "depends_on": [ + "aws_appautoscaling_target.testDynamoDBTable_testDynamoDBTable_dynamodb_table_A3DD49A4_WriteCapacity_table_target_EC052FEE", + "aws_dynamodb_table.testDynamoDBTable_dynamodb_table_A3DD49A4" + ], + "name": "DynamoDBWriteCapacityUtilization:\${aws_appautoscaling_target.testDynamoDBTable_testDynamoDBTable_dynamodb_table_A3DD49A4_WriteCapacity_table_target_EC052FEE.resource_id}", + "policy_type": "TargetTrackingScaling", + "resource_id": "\${aws_appautoscaling_target.testDynamoDBTable_testDynamoDBTable_dynamodb_table_A3DD49A4_WriteCapacity_table_target_EC052FEE.resource_id}", + "scalable_dimension": "\${aws_appautoscaling_target.testDynamoDBTable_testDynamoDBTable_dynamodb_table_A3DD49A4_WriteCapacity_table_target_EC052FEE.scalable_dimension}", + "service_namespace": "\${aws_appautoscaling_target.testDynamoDBTable_testDynamoDBTable_dynamodb_table_A3DD49A4_WriteCapacity_table_target_EC052FEE.service_namespace}", + "target_tracking_scaling_policy_configuration": { + "predefined_metric_specification": { + "predefined_metric_type": "DynamoDBWriteCapacityUtilization" + }, + "target_value": 1 + } + } + }, + "aws_appautoscaling_target": { + "testDynamoDBTable_testDynamoDBTable_dynamodb_table_A3DD49A4_ReadCapacity_table_target_5A95550B": { + "depends_on": [ + "aws_dynamodb_table.testDynamoDBTable_dynamodb_table_A3DD49A4" + ], + "max_capacity": 10, + "min_capacity": 3, + "resource_id": "table/\${aws_dynamodb_table.testDynamoDBTable_dynamodb_table_A3DD49A4.name}", + "role_arn": "\${aws_iam_role.testDynamoDBTable_ReadCapacity_role_2B9645BB.arn}", + "scalable_dimension": "dynamodb:table:ReadCapacityUnits", + "service_namespace": "dynamodb" + }, + "testDynamoDBTable_testDynamoDBTable_dynamodb_table_A3DD49A4_WriteCapacity_table_target_EC052FEE": { + "depends_on": [ + "aws_dynamodb_table.testDynamoDBTable_dynamodb_table_A3DD49A4" + ], + "max_capacity": 10, + "min_capacity": 3, + "resource_id": "table/\${aws_dynamodb_table.testDynamoDBTable_dynamodb_table_A3DD49A4.name}", + "role_arn": "\${aws_iam_role.testDynamoDBTable_WriteCapacity_role_4BB8E3F1.arn}", + "scalable_dimension": "dynamodb:table:WriteCapacityUnits", + "service_namespace": "dynamodb" + } + }, + "aws_dynamodb_table": { + "testDynamoDBTable_dynamodb_table_A3DD49A4": { + "attribute": [ + { + "name": "attribeautiful", + "type": "shrugs!" + } + ], + "billing_mode": "PROVISIONED", + "global_secondary_index": [ + ], + "hash_key": "123", + "lifecycle": { + "ignore_changes": [ + "read_capacity", + "write_capacity" + ], + "prevent_destroy": true + }, + "name": "abides-", + "tags": { + "hobby": "bowling", + "name": "thedude" + } + } + }, + "aws_iam_policy": { + "testDynamoDBTable_ReadCapacity_autoscaling_policy_52DA6649": { + "name": "abides--ReadCapacity-AutoScalingPolicy", + "policy": "\${data.aws_iam_policy_document.testDynamoDBTable_ReadCapacity_policy_document_EF789C68.json}" + }, + "testDynamoDBTable_WriteCapacity_autoscaling_policy_9632CE60": { + "name": "abides--WriteCapacity-AutoScalingPolicy", + "policy": "\${data.aws_iam_policy_document.testDynamoDBTable_WriteCapacity_policy_document_9032D023.json}" + } + }, + "aws_iam_role": { + "testDynamoDBTable_ReadCapacity_role_2B9645BB": { + "assume_role_policy": "\${data.aws_iam_policy_document.testDynamoDBTable_ReadCapacity_assume_role_policy_document_54F698F0.json}", + "name": "abides--ReadCapacity-AutoScalingRole", + "tags": { + "hobby": "bowling", + "name": "thedude" + } + }, + "testDynamoDBTable_WriteCapacity_role_4BB8E3F1": { + "assume_role_policy": "\${data.aws_iam_policy_document.testDynamoDBTable_WriteCapacity_assume_role_policy_document_5A7EA325.json}", + "name": "abides--WriteCapacity-AutoScalingRole", + "tags": { + "hobby": "bowling", + "name": "thedude" + } + } + }, + "aws_iam_role_policy_attachment": { + "testDynamoDBTable_ReadCapacity_role_attachment_3C0829C2": { + "depends_on": [ + "aws_iam_role.testDynamoDBTable_ReadCapacity_role_2B9645BB", + "aws_iam_policy.testDynamoDBTable_ReadCapacity_autoscaling_policy_52DA6649" + ], + "policy_arn": "\${aws_iam_policy.testDynamoDBTable_ReadCapacity_autoscaling_policy_52DA6649.arn}", + "role": "\${aws_iam_role.testDynamoDBTable_ReadCapacity_role_2B9645BB.name}" + }, + "testDynamoDBTable_WriteCapacity_role_attachment_8ABB9833": { + "depends_on": [ + "aws_iam_role.testDynamoDBTable_WriteCapacity_role_4BB8E3F1", + "aws_iam_policy.testDynamoDBTable_WriteCapacity_autoscaling_policy_9632CE60" + ], + "policy_arn": "\${aws_iam_policy.testDynamoDBTable_WriteCapacity_autoscaling_policy_9632CE60.arn}", + "role": "\${aws_iam_role.testDynamoDBTable_WriteCapacity_role_4BB8E3F1.name}" + } + } + } +}" +`; + +exports[`ApplicationDynamoDBTable renders dynamo db table with read capacity 1`] = ` +"{ + "data": { + "aws_iam_policy_document": { + "testDynamoDBTable_ReadCapacity_assume_role_policy_document_54F698F0": { + "statement": [ + { + "actions": [ + "sts:AssumeRole" + ], + "effect": "Allow", + "principals": [ + { + "identifiers": [ + "application-autoscaling.amazonaws.com" + ], + "type": "Service" + } + ] + } + ] + }, + "testDynamoDBTable_ReadCapacity_policy_document_EF789C68": { + "statement": [ + { + "actions": [ + "application-autoscaling:*", + "cloudwatch:DescribeAlarms", + "cloudwatch:PutMetricAlarm" + ], + "effect": "Allow", + "resources": [ + "*" + ] + }, + { + "actions": [ + "dynamodb:DescribeTable", + "dynamodb:UpdateTable" + ], + "effect": "Allow", + "resources": [ + "\${aws_dynamodb_table.testDynamoDBTable_dynamodb_table_A3DD49A4.arn}", + "\${aws_dynamodb_table.testDynamoDBTable_dynamodb_table_A3DD49A4.arn}*" + ] + } + ] + } + } + }, + "resource": { + "aws_appautoscaling_policy": { + "testDynamoDBTable_testDynamoDBTable_dynamodb_table_A3DD49A4_ReadCapacity_table_policy_558BE4C0": { + "depends_on": [ + "aws_appautoscaling_target.testDynamoDBTable_testDynamoDBTable_dynamodb_table_A3DD49A4_ReadCapacity_table_target_5A95550B", + "aws_dynamodb_table.testDynamoDBTable_dynamodb_table_A3DD49A4" + ], + "name": "DynamoDBReadCapacityUtilization:\${aws_appautoscaling_target.testDynamoDBTable_testDynamoDBTable_dynamodb_table_A3DD49A4_ReadCapacity_table_target_5A95550B.resource_id}", + "policy_type": "TargetTrackingScaling", + "resource_id": "\${aws_appautoscaling_target.testDynamoDBTable_testDynamoDBTable_dynamodb_table_A3DD49A4_ReadCapacity_table_target_5A95550B.resource_id}", + "scalable_dimension": "\${aws_appautoscaling_target.testDynamoDBTable_testDynamoDBTable_dynamodb_table_A3DD49A4_ReadCapacity_table_target_5A95550B.scalable_dimension}", + "service_namespace": "\${aws_appautoscaling_target.testDynamoDBTable_testDynamoDBTable_dynamodb_table_A3DD49A4_ReadCapacity_table_target_5A95550B.service_namespace}", + "target_tracking_scaling_policy_configuration": { + "predefined_metric_specification": { + "predefined_metric_type": "DynamoDBReadCapacityUtilization" + }, + "target_value": 1 + } + } + }, + "aws_appautoscaling_target": { + "testDynamoDBTable_testDynamoDBTable_dynamodb_table_A3DD49A4_ReadCapacity_table_target_5A95550B": { + "depends_on": [ + "aws_dynamodb_table.testDynamoDBTable_dynamodb_table_A3DD49A4" + ], + "max_capacity": 10, + "min_capacity": 3, + "resource_id": "table/\${aws_dynamodb_table.testDynamoDBTable_dynamodb_table_A3DD49A4.name}", + "role_arn": "\${aws_iam_role.testDynamoDBTable_ReadCapacity_role_2B9645BB.arn}", + "scalable_dimension": "dynamodb:table:ReadCapacityUnits", + "service_namespace": "dynamodb" + } + }, + "aws_dynamodb_table": { + "testDynamoDBTable_dynamodb_table_A3DD49A4": { + "attribute": [ + { + "name": "attribeautiful", + "type": "shrugs!" + } + ], + "billing_mode": "PROVISIONED", + "global_secondary_index": [ + ], + "hash_key": "123", + "lifecycle": { + "ignore_changes": [ + "read_capacity", + "write_capacity" + ], + "prevent_destroy": true + }, + "name": "abides-" + } + }, + "aws_iam_policy": { + "testDynamoDBTable_ReadCapacity_autoscaling_policy_52DA6649": { + "name": "abides--ReadCapacity-AutoScalingPolicy", + "policy": "\${data.aws_iam_policy_document.testDynamoDBTable_ReadCapacity_policy_document_EF789C68.json}" + } + }, + "aws_iam_role": { + "testDynamoDBTable_ReadCapacity_role_2B9645BB": { + "assume_role_policy": "\${data.aws_iam_policy_document.testDynamoDBTable_ReadCapacity_assume_role_policy_document_54F698F0.json}", + "name": "abides--ReadCapacity-AutoScalingRole" + } + }, + "aws_iam_role_policy_attachment": { + "testDynamoDBTable_ReadCapacity_role_attachment_3C0829C2": { + "depends_on": [ + "aws_iam_role.testDynamoDBTable_ReadCapacity_role_2B9645BB", + "aws_iam_policy.testDynamoDBTable_ReadCapacity_autoscaling_policy_52DA6649" + ], + "policy_arn": "\${aws_iam_policy.testDynamoDBTable_ReadCapacity_autoscaling_policy_52DA6649.arn}", + "role": "\${aws_iam_role.testDynamoDBTable_ReadCapacity_role_2B9645BB.name}" + } + } + } +}" +`; + +exports[`ApplicationDynamoDBTable renders dynamo db table with write capacity 1`] = ` +"{ + "data": { + "aws_iam_policy_document": { + "testDynamoDBTable_WriteCapacity_assume_role_policy_document_5A7EA325": { + "statement": [ + { + "actions": [ + "sts:AssumeRole" + ], + "effect": "Allow", + "principals": [ + { + "identifiers": [ + "application-autoscaling.amazonaws.com" + ], + "type": "Service" + } + ] + } + ] + }, + "testDynamoDBTable_WriteCapacity_policy_document_9032D023": { + "statement": [ + { + "actions": [ + "application-autoscaling:*", + "cloudwatch:DescribeAlarms", + "cloudwatch:PutMetricAlarm" + ], + "effect": "Allow", + "resources": [ + "*" + ] + }, + { + "actions": [ + "dynamodb:DescribeTable", + "dynamodb:UpdateTable" + ], + "effect": "Allow", + "resources": [ + "\${aws_dynamodb_table.testDynamoDBTable_dynamodb_table_A3DD49A4.arn}", + "\${aws_dynamodb_table.testDynamoDBTable_dynamodb_table_A3DD49A4.arn}*" + ] + } + ] + } + } + }, + "resource": { + "aws_appautoscaling_policy": { + "testDynamoDBTable_testDynamoDBTable_dynamodb_table_A3DD49A4_WriteCapacity_table_policy_45352666": { + "depends_on": [ + "aws_appautoscaling_target.testDynamoDBTable_testDynamoDBTable_dynamodb_table_A3DD49A4_WriteCapacity_table_target_EC052FEE", + "aws_dynamodb_table.testDynamoDBTable_dynamodb_table_A3DD49A4" + ], + "name": "DynamoDBWriteCapacityUtilization:\${aws_appautoscaling_target.testDynamoDBTable_testDynamoDBTable_dynamodb_table_A3DD49A4_WriteCapacity_table_target_EC052FEE.resource_id}", + "policy_type": "TargetTrackingScaling", + "resource_id": "\${aws_appautoscaling_target.testDynamoDBTable_testDynamoDBTable_dynamodb_table_A3DD49A4_WriteCapacity_table_target_EC052FEE.resource_id}", + "scalable_dimension": "\${aws_appautoscaling_target.testDynamoDBTable_testDynamoDBTable_dynamodb_table_A3DD49A4_WriteCapacity_table_target_EC052FEE.scalable_dimension}", + "service_namespace": "\${aws_appautoscaling_target.testDynamoDBTable_testDynamoDBTable_dynamodb_table_A3DD49A4_WriteCapacity_table_target_EC052FEE.service_namespace}", + "target_tracking_scaling_policy_configuration": { + "predefined_metric_specification": { + "predefined_metric_type": "DynamoDBWriteCapacityUtilization" + }, + "target_value": 1 + } + } + }, + "aws_appautoscaling_target": { + "testDynamoDBTable_testDynamoDBTable_dynamodb_table_A3DD49A4_WriteCapacity_table_target_EC052FEE": { + "depends_on": [ + "aws_dynamodb_table.testDynamoDBTable_dynamodb_table_A3DD49A4" + ], + "max_capacity": 10, + "min_capacity": 3, + "resource_id": "table/\${aws_dynamodb_table.testDynamoDBTable_dynamodb_table_A3DD49A4.name}", + "role_arn": "\${aws_iam_role.testDynamoDBTable_WriteCapacity_role_4BB8E3F1.arn}", + "scalable_dimension": "dynamodb:table:WriteCapacityUnits", + "service_namespace": "dynamodb" + } + }, + "aws_dynamodb_table": { + "testDynamoDBTable_dynamodb_table_A3DD49A4": { + "attribute": [ + { + "name": "attribeautiful", + "type": "shrugs!" + } + ], + "billing_mode": "PROVISIONED", + "global_secondary_index": [ + ], + "hash_key": "123", + "lifecycle": { + "ignore_changes": [ + "read_capacity", + "write_capacity" + ], + "prevent_destroy": true + }, + "name": "abides-" + } + }, + "aws_iam_policy": { + "testDynamoDBTable_WriteCapacity_autoscaling_policy_9632CE60": { + "name": "abides--WriteCapacity-AutoScalingPolicy", + "policy": "\${data.aws_iam_policy_document.testDynamoDBTable_WriteCapacity_policy_document_9032D023.json}" + } + }, + "aws_iam_role": { + "testDynamoDBTable_WriteCapacity_role_4BB8E3F1": { + "assume_role_policy": "\${data.aws_iam_policy_document.testDynamoDBTable_WriteCapacity_assume_role_policy_document_5A7EA325.json}", + "name": "abides--WriteCapacity-AutoScalingRole" + } + }, + "aws_iam_role_policy_attachment": { + "testDynamoDBTable_WriteCapacity_role_attachment_8ABB9833": { + "depends_on": [ + "aws_iam_role.testDynamoDBTable_WriteCapacity_role_4BB8E3F1", + "aws_iam_policy.testDynamoDBTable_WriteCapacity_autoscaling_policy_9632CE60" + ], + "policy_arn": "\${aws_iam_policy.testDynamoDBTable_WriteCapacity_autoscaling_policy_9632CE60.arn}", + "role": "\${aws_iam_role.testDynamoDBTable_WriteCapacity_role_4BB8E3F1.name}" + } + } + } +}" +`; diff --git a/packages/terraform-modules/src/base/__snapshots__/ApplicationECR.spec.ts.snap b/packages/terraform-modules/src/base/__snapshots__/ApplicationECR.spec.ts.snap new file mode 100644 index 00000000..48d77caa --- /dev/null +++ b/packages/terraform-modules/src/base/__snapshots__/ApplicationECR.spec.ts.snap @@ -0,0 +1,53 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ApplicationECR renders an ECR with tags 1`] = ` +"{ + "resource": { + "aws_ecr_lifecycle_policy": { + "testECR_ecr-repo-lifecyclepolicy_4B354AF7": { + "depends_on": [ + "aws_ecr_repository.testECR_ecr-repo_E42B68DA" + ], + "policy": "{\\"rules\\":[{\\"rulePriority\\":1,\\"description\\":\\"expire old images\\",\\"selection\\":{\\"tagStatus\\":\\"any\\",\\"countType\\":\\"imageCountMoreThan\\",\\"countNumber\\":800},\\"action\\":{\\"type\\":\\"expire\\"}}]}", + "repository": "\${aws_ecr_repository.testECR_ecr-repo_E42B68DA.name}" + } + }, + "aws_ecr_repository": { + "testECR_ecr-repo_E42B68DA": { + "image_scanning_configuration": { + "scan_on_push": true + }, + "name": "bowling", + "tags": { + "description": "tiedtheroomtogether", + "name": "rug" + } + } + } + } +}" +`; + +exports[`ApplicationECR renders an ECR without tags 1`] = ` +"{ + "resource": { + "aws_ecr_lifecycle_policy": { + "testECR_ecr-repo-lifecyclepolicy_4B354AF7": { + "depends_on": [ + "aws_ecr_repository.testECR_ecr-repo_E42B68DA" + ], + "policy": "{\\"rules\\":[{\\"rulePriority\\":1,\\"description\\":\\"expire old images\\",\\"selection\\":{\\"tagStatus\\":\\"any\\",\\"countType\\":\\"imageCountMoreThan\\",\\"countNumber\\":800},\\"action\\":{\\"type\\":\\"expire\\"}}]}", + "repository": "\${aws_ecr_repository.testECR_ecr-repo_E42B68DA.name}" + } + }, + "aws_ecr_repository": { + "testECR_ecr-repo_E42B68DA": { + "image_scanning_configuration": { + "scan_on_push": true + }, + "name": "bowling" + } + } + } +}" +`; diff --git a/packages/terraform-modules/src/base/__snapshots__/ApplicationECSAlbCodeDeploy.spec.ts.snap b/packages/terraform-modules/src/base/__snapshots__/ApplicationECSAlbCodeDeploy.spec.ts.snap new file mode 100644 index 00000000..d23ac817 --- /dev/null +++ b/packages/terraform-modules/src/base/__snapshots__/ApplicationECSAlbCodeDeploy.spec.ts.snap @@ -0,0 +1,908 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ApplicationECSAlbCodeDeploy renders a CodeDeploy with a sns 1`] = ` +"{ + "data": { + "aws_caller_identity": { + "testCodeDeploy_current_account_0C0AC491": { + } + }, + "aws_iam_policy_document": { + "testCodeDeploy_codedeploy_assume_role_E9FEDA7F": { + "statement": [ + { + "actions": [ + "sts:AssumeRole" + ], + "effect": "Allow", + "principals": [ + { + "identifiers": [ + "codedeploy.amazonaws.com" + ], + "type": "Service" + } + ] + } + ] + } + }, + "aws_region": { + "testCodeDeploy_current_region_B8533BBF": { + } + } + }, + "resource": { + "aws_codedeploy_app": { + "testCodeDeploy_ecs_code_deploy_C4535318": { + "compute_platform": "ECS", + "name": "Test-Dev-ECS" + } + }, + "aws_codedeploy_deployment_group": { + "testCodeDeploy_ecs_codedeploy_deployment_group_F092EC5E": { + "app_name": "\${aws_codedeploy_app.testCodeDeploy_ecs_code_deploy_C4535318.name}", + "auto_rollback_configuration": { + "enabled": true, + "events": [ + "DEPLOYMENT_FAILURE" + ] + }, + "blue_green_deployment_config": { + "deployment_ready_option": { + "action_on_timeout": "CONTINUE_DEPLOYMENT" + }, + "terminate_blue_instances_on_deployment_success": { + "action": "TERMINATE", + "termination_wait_time_in_minutes": 5 + } + }, + "deployment_config_name": "CodeDeployDefault.ECSAllAtOnce", + "deployment_group_name": "Test-Dev-ECS", + "deployment_style": { + "deployment_option": "WITH_TRAFFIC_CONTROL", + "deployment_type": "BLUE_GREEN" + }, + "ecs_service": { + "cluster_name": "cluster", + "service_name": "theService" + }, + "load_balancer_info": { + "target_group_pair_info": { + "prod_traffic_route": { + "listener_arns": [ + "listen-to-me" + ] + }, + "target_group": [ + { + "name": "target-1" + }, + { + "name": "target-2" + } + ] + } + }, + "service_role_arn": "\${aws_iam_role.testCodeDeploy_ecs_code_deploy_role_EF679F94.arn}" + } + }, + "aws_codestarnotifications_notification_rule": { + "testCodeDeploy_ecs_codedeploy_notifications_7223DA1D": { + "detail_type": "BASIC", + "event_type_ids": [ + "codedeploy-application-deployment-failed", + "codedeploy-application-deployment-succeeded", + "codedeploy-application-deployment-started" + ], + "name": "\${aws_codedeploy_app.testCodeDeploy_ecs_code_deploy_C4535318.name}", + "resource": "arn:aws:codedeploy:\${data.aws_region.testCodeDeploy_current_region_B8533BBF.name}:\${data.aws_caller_identity.testCodeDeploy_current_account_0C0AC491.account_id}:application:\${aws_codedeploy_app.testCodeDeploy_ecs_code_deploy_C4535318.name}", + "target": [ + { + "address": "notify-me" + } + ] + } + }, + "aws_iam_role": { + "testCodeDeploy_ecs_code_deploy_role_EF679F94": { + "assume_role_policy": "\${data.aws_iam_policy_document.testCodeDeploy_codedeploy_assume_role_E9FEDA7F.json}", + "name": "Test-Dev-ECSCodeDeployRole" + } + }, + "aws_iam_role_policy_attachment": { + "testCodeDeploy_ecs_codedeploy_role_attachment_0C2B6B97": { + "depends_on": [ + "aws_iam_role.testCodeDeploy_ecs_code_deploy_role_EF679F94" + ], + "policy_arn": "arn:aws:iam::aws:policy/AWSCodeDeployRoleForECS", + "role": "\${aws_iam_role.testCodeDeploy_ecs_code_deploy_role_EF679F94.name}" + } + } + } +}" +`; + +exports[`ApplicationECSAlbCodeDeploy renders a CodeDeploy with no deploy notifications 1`] = ` +"{ + "data": { + "aws_caller_identity": { + "testCodeDeploy_current_account_0C0AC491": { + } + }, + "aws_iam_policy_document": { + "testCodeDeploy_codedeploy_assume_role_E9FEDA7F": { + "statement": [ + { + "actions": [ + "sts:AssumeRole" + ], + "effect": "Allow", + "principals": [ + { + "identifiers": [ + "codedeploy.amazonaws.com" + ], + "type": "Service" + } + ] + } + ] + } + }, + "aws_region": { + "testCodeDeploy_current_region_B8533BBF": { + } + } + }, + "resource": { + "aws_codedeploy_app": { + "testCodeDeploy_ecs_code_deploy_C4535318": { + "compute_platform": "ECS", + "name": "Test-Dev-ECS", + "tags": { + "tag": "me", + "test": "1234" + } + } + }, + "aws_codedeploy_deployment_group": { + "testCodeDeploy_ecs_codedeploy_deployment_group_F092EC5E": { + "app_name": "\${aws_codedeploy_app.testCodeDeploy_ecs_code_deploy_C4535318.name}", + "auto_rollback_configuration": { + "enabled": true, + "events": [ + "DEPLOYMENT_FAILURE" + ] + }, + "blue_green_deployment_config": { + "deployment_ready_option": { + "action_on_timeout": "CONTINUE_DEPLOYMENT" + }, + "terminate_blue_instances_on_deployment_success": { + "action": "TERMINATE", + "termination_wait_time_in_minutes": 5 + } + }, + "deployment_config_name": "CodeDeployDefault.ECSAllAtOnce", + "deployment_group_name": "Test-Dev-ECS", + "deployment_style": { + "deployment_option": "WITH_TRAFFIC_CONTROL", + "deployment_type": "BLUE_GREEN" + }, + "ecs_service": { + "cluster_name": "cluster", + "service_name": "theService" + }, + "load_balancer_info": { + "target_group_pair_info": { + "prod_traffic_route": { + "listener_arns": [ + "listen-to-me" + ] + }, + "target_group": [ + { + "name": "target-1" + }, + { + "name": "target-2" + } + ] + } + }, + "service_role_arn": "\${aws_iam_role.testCodeDeploy_ecs_code_deploy_role_EF679F94.arn}", + "tags": { + "tag": "me", + "test": "1234" + } + } + }, + "aws_codestarnotifications_notification_rule": { + "testCodeDeploy_ecs_codedeploy_notifications_7223DA1D": { + "detail_type": "BASIC", + "event_type_ids": [ + ], + "name": "\${aws_codedeploy_app.testCodeDeploy_ecs_code_deploy_C4535318.name}", + "resource": "arn:aws:codedeploy:\${data.aws_region.testCodeDeploy_current_region_B8533BBF.name}:\${data.aws_caller_identity.testCodeDeploy_current_account_0C0AC491.account_id}:application:\${aws_codedeploy_app.testCodeDeploy_ecs_code_deploy_C4535318.name}", + "tags": { + "tag": "me", + "test": "1234" + }, + "target": [ + { + "address": "notify-me" + } + ] + } + }, + "aws_iam_role": { + "testCodeDeploy_ecs_code_deploy_role_EF679F94": { + "assume_role_policy": "\${data.aws_iam_policy_document.testCodeDeploy_codedeploy_assume_role_E9FEDA7F.json}", + "name": "Test-Dev-ECSCodeDeployRole", + "tags": { + "tag": "me", + "test": "1234" + } + } + }, + "aws_iam_role_policy_attachment": { + "testCodeDeploy_ecs_codedeploy_role_attachment_0C2B6B97": { + "depends_on": [ + "aws_iam_role.testCodeDeploy_ecs_code_deploy_role_EF679F94" + ], + "policy_arn": "arn:aws:iam::aws:policy/AWSCodeDeployRoleForECS", + "role": "\${aws_iam_role.testCodeDeploy_ecs_code_deploy_role_EF679F94.name}" + } + } + } +}" +`; + +exports[`ApplicationECSAlbCodeDeploy renders a CodeDeploy with only failed deploy notifications 1`] = ` +"{ + "data": { + "aws_caller_identity": { + "testCodeDeploy_current_account_0C0AC491": { + } + }, + "aws_iam_policy_document": { + "testCodeDeploy_codedeploy_assume_role_E9FEDA7F": { + "statement": [ + { + "actions": [ + "sts:AssumeRole" + ], + "effect": "Allow", + "principals": [ + { + "identifiers": [ + "codedeploy.amazonaws.com" + ], + "type": "Service" + } + ] + } + ] + } + }, + "aws_region": { + "testCodeDeploy_current_region_B8533BBF": { + } + } + }, + "resource": { + "aws_codedeploy_app": { + "testCodeDeploy_ecs_code_deploy_C4535318": { + "compute_platform": "ECS", + "name": "Test-Dev-ECS", + "tags": { + "tag": "me", + "test": "1234" + } + } + }, + "aws_codedeploy_deployment_group": { + "testCodeDeploy_ecs_codedeploy_deployment_group_F092EC5E": { + "app_name": "\${aws_codedeploy_app.testCodeDeploy_ecs_code_deploy_C4535318.name}", + "auto_rollback_configuration": { + "enabled": true, + "events": [ + "DEPLOYMENT_FAILURE" + ] + }, + "blue_green_deployment_config": { + "deployment_ready_option": { + "action_on_timeout": "CONTINUE_DEPLOYMENT" + }, + "terminate_blue_instances_on_deployment_success": { + "action": "TERMINATE", + "termination_wait_time_in_minutes": 5 + } + }, + "deployment_config_name": "CodeDeployDefault.ECSAllAtOnce", + "deployment_group_name": "Test-Dev-ECS", + "deployment_style": { + "deployment_option": "WITH_TRAFFIC_CONTROL", + "deployment_type": "BLUE_GREEN" + }, + "ecs_service": { + "cluster_name": "cluster", + "service_name": "theService" + }, + "load_balancer_info": { + "target_group_pair_info": { + "prod_traffic_route": { + "listener_arns": [ + "listen-to-me" + ] + }, + "target_group": [ + { + "name": "target-1" + }, + { + "name": "target-2" + } + ] + } + }, + "service_role_arn": "\${aws_iam_role.testCodeDeploy_ecs_code_deploy_role_EF679F94.arn}", + "tags": { + "tag": "me", + "test": "1234" + } + } + }, + "aws_codestarnotifications_notification_rule": { + "testCodeDeploy_ecs_codedeploy_notifications_7223DA1D": { + "detail_type": "BASIC", + "event_type_ids": [ + "codedeploy-application-deployment-failed" + ], + "name": "\${aws_codedeploy_app.testCodeDeploy_ecs_code_deploy_C4535318.name}", + "resource": "arn:aws:codedeploy:\${data.aws_region.testCodeDeploy_current_region_B8533BBF.name}:\${data.aws_caller_identity.testCodeDeploy_current_account_0C0AC491.account_id}:application:\${aws_codedeploy_app.testCodeDeploy_ecs_code_deploy_C4535318.name}", + "tags": { + "tag": "me", + "test": "1234" + }, + "target": [ + { + "address": "notify-me" + } + ] + } + }, + "aws_iam_role": { + "testCodeDeploy_ecs_code_deploy_role_EF679F94": { + "assume_role_policy": "\${data.aws_iam_policy_document.testCodeDeploy_codedeploy_assume_role_E9FEDA7F.json}", + "name": "Test-Dev-ECSCodeDeployRole", + "tags": { + "tag": "me", + "test": "1234" + } + } + }, + "aws_iam_role_policy_attachment": { + "testCodeDeploy_ecs_codedeploy_role_attachment_0C2B6B97": { + "depends_on": [ + "aws_iam_role.testCodeDeploy_ecs_code_deploy_role_EF679F94" + ], + "policy_arn": "arn:aws:iam::aws:policy/AWSCodeDeployRoleForECS", + "role": "\${aws_iam_role.testCodeDeploy_ecs_code_deploy_role_EF679F94.name}" + } + } + } +}" +`; + +exports[`ApplicationECSAlbCodeDeploy renders a CodeDeploy with only started deploy notifications 1`] = ` +"{ + "data": { + "aws_caller_identity": { + "testCodeDeploy_current_account_0C0AC491": { + } + }, + "aws_iam_policy_document": { + "testCodeDeploy_codedeploy_assume_role_E9FEDA7F": { + "statement": [ + { + "actions": [ + "sts:AssumeRole" + ], + "effect": "Allow", + "principals": [ + { + "identifiers": [ + "codedeploy.amazonaws.com" + ], + "type": "Service" + } + ] + } + ] + } + }, + "aws_region": { + "testCodeDeploy_current_region_B8533BBF": { + } + } + }, + "resource": { + "aws_codedeploy_app": { + "testCodeDeploy_ecs_code_deploy_C4535318": { + "compute_platform": "ECS", + "name": "Test-Dev-ECS", + "tags": { + "tag": "me", + "test": "1234" + } + } + }, + "aws_codedeploy_deployment_group": { + "testCodeDeploy_ecs_codedeploy_deployment_group_F092EC5E": { + "app_name": "\${aws_codedeploy_app.testCodeDeploy_ecs_code_deploy_C4535318.name}", + "auto_rollback_configuration": { + "enabled": true, + "events": [ + "DEPLOYMENT_FAILURE" + ] + }, + "blue_green_deployment_config": { + "deployment_ready_option": { + "action_on_timeout": "CONTINUE_DEPLOYMENT" + }, + "terminate_blue_instances_on_deployment_success": { + "action": "TERMINATE", + "termination_wait_time_in_minutes": 5 + } + }, + "deployment_config_name": "CodeDeployDefault.ECSAllAtOnce", + "deployment_group_name": "Test-Dev-ECS", + "deployment_style": { + "deployment_option": "WITH_TRAFFIC_CONTROL", + "deployment_type": "BLUE_GREEN" + }, + "ecs_service": { + "cluster_name": "cluster", + "service_name": "theService" + }, + "load_balancer_info": { + "target_group_pair_info": { + "prod_traffic_route": { + "listener_arns": [ + "listen-to-me" + ] + }, + "target_group": [ + { + "name": "target-1" + }, + { + "name": "target-2" + } + ] + } + }, + "service_role_arn": "\${aws_iam_role.testCodeDeploy_ecs_code_deploy_role_EF679F94.arn}", + "tags": { + "tag": "me", + "test": "1234" + } + } + }, + "aws_codestarnotifications_notification_rule": { + "testCodeDeploy_ecs_codedeploy_notifications_7223DA1D": { + "detail_type": "BASIC", + "event_type_ids": [ + "codedeploy-application-deployment-started" + ], + "name": "\${aws_codedeploy_app.testCodeDeploy_ecs_code_deploy_C4535318.name}", + "resource": "arn:aws:codedeploy:\${data.aws_region.testCodeDeploy_current_region_B8533BBF.name}:\${data.aws_caller_identity.testCodeDeploy_current_account_0C0AC491.account_id}:application:\${aws_codedeploy_app.testCodeDeploy_ecs_code_deploy_C4535318.name}", + "tags": { + "tag": "me", + "test": "1234" + }, + "target": [ + { + "address": "notify-me" + } + ] + } + }, + "aws_iam_role": { + "testCodeDeploy_ecs_code_deploy_role_EF679F94": { + "assume_role_policy": "\${data.aws_iam_policy_document.testCodeDeploy_codedeploy_assume_role_E9FEDA7F.json}", + "name": "Test-Dev-ECSCodeDeployRole", + "tags": { + "tag": "me", + "test": "1234" + } + } + }, + "aws_iam_role_policy_attachment": { + "testCodeDeploy_ecs_codedeploy_role_attachment_0C2B6B97": { + "depends_on": [ + "aws_iam_role.testCodeDeploy_ecs_code_deploy_role_EF679F94" + ], + "policy_arn": "arn:aws:iam::aws:policy/AWSCodeDeployRoleForECS", + "role": "\${aws_iam_role.testCodeDeploy_ecs_code_deploy_role_EF679F94.name}" + } + } + } +}" +`; + +exports[`ApplicationECSAlbCodeDeploy renders a CodeDeploy with only succeeded deploy notifications 1`] = ` +"{ + "data": { + "aws_caller_identity": { + "testCodeDeploy_current_account_0C0AC491": { + } + }, + "aws_iam_policy_document": { + "testCodeDeploy_codedeploy_assume_role_E9FEDA7F": { + "statement": [ + { + "actions": [ + "sts:AssumeRole" + ], + "effect": "Allow", + "principals": [ + { + "identifiers": [ + "codedeploy.amazonaws.com" + ], + "type": "Service" + } + ] + } + ] + } + }, + "aws_region": { + "testCodeDeploy_current_region_B8533BBF": { + } + } + }, + "resource": { + "aws_codedeploy_app": { + "testCodeDeploy_ecs_code_deploy_C4535318": { + "compute_platform": "ECS", + "name": "Test-Dev-ECS", + "tags": { + "tag": "me", + "test": "1234" + } + } + }, + "aws_codedeploy_deployment_group": { + "testCodeDeploy_ecs_codedeploy_deployment_group_F092EC5E": { + "app_name": "\${aws_codedeploy_app.testCodeDeploy_ecs_code_deploy_C4535318.name}", + "auto_rollback_configuration": { + "enabled": true, + "events": [ + "DEPLOYMENT_FAILURE" + ] + }, + "blue_green_deployment_config": { + "deployment_ready_option": { + "action_on_timeout": "CONTINUE_DEPLOYMENT" + }, + "terminate_blue_instances_on_deployment_success": { + "action": "TERMINATE", + "termination_wait_time_in_minutes": 5 + } + }, + "deployment_config_name": "CodeDeployDefault.ECSAllAtOnce", + "deployment_group_name": "Test-Dev-ECS", + "deployment_style": { + "deployment_option": "WITH_TRAFFIC_CONTROL", + "deployment_type": "BLUE_GREEN" + }, + "ecs_service": { + "cluster_name": "cluster", + "service_name": "theService" + }, + "load_balancer_info": { + "target_group_pair_info": { + "prod_traffic_route": { + "listener_arns": [ + "listen-to-me" + ] + }, + "target_group": [ + { + "name": "target-1" + }, + { + "name": "target-2" + } + ] + } + }, + "service_role_arn": "\${aws_iam_role.testCodeDeploy_ecs_code_deploy_role_EF679F94.arn}", + "tags": { + "tag": "me", + "test": "1234" + } + } + }, + "aws_codestarnotifications_notification_rule": { + "testCodeDeploy_ecs_codedeploy_notifications_7223DA1D": { + "detail_type": "BASIC", + "event_type_ids": [ + "codedeploy-application-deployment-succeeded" + ], + "name": "\${aws_codedeploy_app.testCodeDeploy_ecs_code_deploy_C4535318.name}", + "resource": "arn:aws:codedeploy:\${data.aws_region.testCodeDeploy_current_region_B8533BBF.name}:\${data.aws_caller_identity.testCodeDeploy_current_account_0C0AC491.account_id}:application:\${aws_codedeploy_app.testCodeDeploy_ecs_code_deploy_C4535318.name}", + "tags": { + "tag": "me", + "test": "1234" + }, + "target": [ + { + "address": "notify-me" + } + ] + } + }, + "aws_iam_role": { + "testCodeDeploy_ecs_code_deploy_role_EF679F94": { + "assume_role_policy": "\${data.aws_iam_policy_document.testCodeDeploy_codedeploy_assume_role_E9FEDA7F.json}", + "name": "Test-Dev-ECSCodeDeployRole", + "tags": { + "tag": "me", + "test": "1234" + } + } + }, + "aws_iam_role_policy_attachment": { + "testCodeDeploy_ecs_codedeploy_role_attachment_0C2B6B97": { + "depends_on": [ + "aws_iam_role.testCodeDeploy_ecs_code_deploy_role_EF679F94" + ], + "policy_arn": "arn:aws:iam::aws:policy/AWSCodeDeployRoleForECS", + "role": "\${aws_iam_role.testCodeDeploy_ecs_code_deploy_role_EF679F94.name}" + } + } + } +}" +`; + +exports[`ApplicationECSAlbCodeDeploy renders a CodeDeploy with tags 1`] = ` +"{ + "data": { + "aws_caller_identity": { + "testCodeDeploy_current_account_0C0AC491": { + } + }, + "aws_iam_policy_document": { + "testCodeDeploy_codedeploy_assume_role_E9FEDA7F": { + "statement": [ + { + "actions": [ + "sts:AssumeRole" + ], + "effect": "Allow", + "principals": [ + { + "identifiers": [ + "codedeploy.amazonaws.com" + ], + "type": "Service" + } + ] + } + ] + } + }, + "aws_region": { + "testCodeDeploy_current_region_B8533BBF": { + } + } + }, + "resource": { + "aws_codedeploy_app": { + "testCodeDeploy_ecs_code_deploy_C4535318": { + "compute_platform": "ECS", + "name": "Test-Dev-ECS", + "tags": { + "tag": "me", + "test": "1234" + } + } + }, + "aws_codedeploy_deployment_group": { + "testCodeDeploy_ecs_codedeploy_deployment_group_F092EC5E": { + "app_name": "\${aws_codedeploy_app.testCodeDeploy_ecs_code_deploy_C4535318.name}", + "auto_rollback_configuration": { + "enabled": true, + "events": [ + "DEPLOYMENT_FAILURE" + ] + }, + "blue_green_deployment_config": { + "deployment_ready_option": { + "action_on_timeout": "CONTINUE_DEPLOYMENT" + }, + "terminate_blue_instances_on_deployment_success": { + "action": "TERMINATE", + "termination_wait_time_in_minutes": 5 + } + }, + "deployment_config_name": "CodeDeployDefault.ECSAllAtOnce", + "deployment_group_name": "Test-Dev-ECS", + "deployment_style": { + "deployment_option": "WITH_TRAFFIC_CONTROL", + "deployment_type": "BLUE_GREEN" + }, + "ecs_service": { + "cluster_name": "cluster", + "service_name": "theService" + }, + "load_balancer_info": { + "target_group_pair_info": { + "prod_traffic_route": { + "listener_arns": [ + "listen-to-me" + ] + }, + "target_group": [ + { + "name": "target-1" + }, + { + "name": "target-2" + } + ] + } + }, + "service_role_arn": "\${aws_iam_role.testCodeDeploy_ecs_code_deploy_role_EF679F94.arn}", + "tags": { + "tag": "me", + "test": "1234" + } + } + }, + "aws_codestarnotifications_notification_rule": { + "testCodeDeploy_ecs_codedeploy_notifications_7223DA1D": { + "detail_type": "BASIC", + "event_type_ids": [ + "codedeploy-application-deployment-failed", + "codedeploy-application-deployment-succeeded", + "codedeploy-application-deployment-started" + ], + "name": "\${aws_codedeploy_app.testCodeDeploy_ecs_code_deploy_C4535318.name}", + "resource": "arn:aws:codedeploy:\${data.aws_region.testCodeDeploy_current_region_B8533BBF.name}:\${data.aws_caller_identity.testCodeDeploy_current_account_0C0AC491.account_id}:application:\${aws_codedeploy_app.testCodeDeploy_ecs_code_deploy_C4535318.name}", + "tags": { + "tag": "me", + "test": "1234" + }, + "target": [ + { + "address": "notify-me" + } + ] + } + }, + "aws_iam_role": { + "testCodeDeploy_ecs_code_deploy_role_EF679F94": { + "assume_role_policy": "\${data.aws_iam_policy_document.testCodeDeploy_codedeploy_assume_role_E9FEDA7F.json}", + "name": "Test-Dev-ECSCodeDeployRole", + "tags": { + "tag": "me", + "test": "1234" + } + } + }, + "aws_iam_role_policy_attachment": { + "testCodeDeploy_ecs_codedeploy_role_attachment_0C2B6B97": { + "depends_on": [ + "aws_iam_role.testCodeDeploy_ecs_code_deploy_role_EF679F94" + ], + "policy_arn": "arn:aws:iam::aws:policy/AWSCodeDeployRoleForECS", + "role": "\${aws_iam_role.testCodeDeploy_ecs_code_deploy_role_EF679F94.name}" + } + } + } +}" +`; + +exports[`ApplicationECSAlbCodeDeploy renders a CodeDeploy without a sns 1`] = ` +"{ + "data": { + "aws_iam_policy_document": { + "testCodeDeploy_codedeploy_assume_role_E9FEDA7F": { + "statement": [ + { + "actions": [ + "sts:AssumeRole" + ], + "effect": "Allow", + "principals": [ + { + "identifiers": [ + "codedeploy.amazonaws.com" + ], + "type": "Service" + } + ] + } + ] + } + } + }, + "resource": { + "aws_codedeploy_app": { + "testCodeDeploy_ecs_code_deploy_C4535318": { + "compute_platform": "ECS", + "name": "Test-Dev-ECS" + } + }, + "aws_codedeploy_deployment_group": { + "testCodeDeploy_ecs_codedeploy_deployment_group_F092EC5E": { + "app_name": "\${aws_codedeploy_app.testCodeDeploy_ecs_code_deploy_C4535318.name}", + "auto_rollback_configuration": { + "enabled": true, + "events": [ + "DEPLOYMENT_FAILURE" + ] + }, + "blue_green_deployment_config": { + "deployment_ready_option": { + "action_on_timeout": "CONTINUE_DEPLOYMENT" + }, + "terminate_blue_instances_on_deployment_success": { + "action": "TERMINATE", + "termination_wait_time_in_minutes": 5 + } + }, + "deployment_config_name": "CodeDeployDefault.ECSAllAtOnce", + "deployment_group_name": "Test-Dev-ECS", + "deployment_style": { + "deployment_option": "WITH_TRAFFIC_CONTROL", + "deployment_type": "BLUE_GREEN" + }, + "ecs_service": { + "cluster_name": "cluster", + "service_name": "theService" + }, + "load_balancer_info": { + "target_group_pair_info": { + "prod_traffic_route": { + "listener_arns": [ + "listen-to-me" + ] + }, + "target_group": [ + { + "name": "target-1" + }, + { + "name": "target-2" + } + ] + } + }, + "service_role_arn": "\${aws_iam_role.testCodeDeploy_ecs_code_deploy_role_EF679F94.arn}" + } + }, + "aws_iam_role": { + "testCodeDeploy_ecs_code_deploy_role_EF679F94": { + "assume_role_policy": "\${data.aws_iam_policy_document.testCodeDeploy_codedeploy_assume_role_E9FEDA7F.json}", + "name": "Test-Dev-ECSCodeDeployRole" + } + }, + "aws_iam_role_policy_attachment": { + "testCodeDeploy_ecs_codedeploy_role_attachment_0C2B6B97": { + "depends_on": [ + "aws_iam_role.testCodeDeploy_ecs_code_deploy_role_EF679F94" + ], + "policy_arn": "arn:aws:iam::aws:policy/AWSCodeDeployRoleForECS", + "role": "\${aws_iam_role.testCodeDeploy_ecs_code_deploy_role_EF679F94.name}" + } + } + } +}" +`; diff --git a/packages/terraform-modules/src/base/__snapshots__/ApplicationECSCluster.spec.ts.snap b/packages/terraform-modules/src/base/__snapshots__/ApplicationECSCluster.spec.ts.snap new file mode 100644 index 00000000..ebacb7fa --- /dev/null +++ b/packages/terraform-modules/src/base/__snapshots__/ApplicationECSCluster.spec.ts.snap @@ -0,0 +1,41 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ApplicationECSCluster renders an ECS cluster with tags 1`] = ` +"{ + "resource": { + "aws_ecs_cluster": { + "testECSCluster_ecs_cluster_E3A9AFE5": { + "name": "bowling-", + "setting": [ + { + "name": "containerInsights", + "value": "enabled" + } + ], + "tags": { + "description": "artist", + "name": "maude" + } + } + } + } +}" +`; + +exports[`ApplicationECSCluster renders an ECS cluster without tags 1`] = ` +"{ + "resource": { + "aws_ecs_cluster": { + "testECSCluster_ecs_cluster_E3A9AFE5": { + "name": "bowling-", + "setting": [ + { + "name": "containerInsights", + "value": "enabled" + } + ] + } + } + } +}" +`; diff --git a/packages/terraform-modules/src/base/__snapshots__/ApplicationECSIAM.spec.ts.snap b/packages/terraform-modules/src/base/__snapshots__/ApplicationECSIAM.spec.ts.snap new file mode 100644 index 00000000..8fe0f5f9 --- /dev/null +++ b/packages/terraform-modules/src/base/__snapshots__/ApplicationECSIAM.spec.ts.snap @@ -0,0 +1,101 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ApplicationECSIAM renders ECS IAM with minimal config 1`] = ` +"{ + "data": { + "aws_iam_policy_document": { + "testECSService_ecs-task-assume_E7A7BA71": { + "statement": [ + { + "actions": [ + "sts:AssumeRole" + ], + "effect": "Allow", + "principals": [ + { + "identifiers": [ + "ecs-tasks.amazonaws.com" + ], + "type": "Service" + } + ] + } + ], + "version": "2012-10-17" + } + } + }, + "resource": { + "aws_iam_role": { + "testECSService_ecs-execution-role_46F1514F": { + "assume_role_policy": "\${data.aws_iam_policy_document.testECSService_ecs-task-assume_E7A7BA71.json}", + "name": "abides-dev-TaskExecutionRole" + }, + "testECSService_ecs-task-role_C04CE6DA": { + "assume_role_policy": "\${data.aws_iam_policy_document.testECSService_ecs-task-assume_E7A7BA71.json}", + "name": "abides-dev-TaskRole" + } + }, + "aws_iam_role_policy_attachment": { + "testECSService_ecs-task-execution-default-attachment_9FDF40D5": { + "policy_arn": "someArn", + "role": "\${aws_iam_role.testECSService_ecs-execution-role_46F1514F.id}" + } + } + } +}" +`; + +exports[`ApplicationECSIAM renders ECS IAM with tags 1`] = ` +"{ + "data": { + "aws_iam_policy_document": { + "testECSService_ecs-task-assume_E7A7BA71": { + "statement": [ + { + "actions": [ + "sts:AssumeRole" + ], + "effect": "Allow", + "principals": [ + { + "identifiers": [ + "ecs-tasks.amazonaws.com" + ], + "type": "Service" + } + ] + } + ], + "version": "2012-10-17" + } + } + }, + "resource": { + "aws_iam_role": { + "testECSService_ecs-execution-role_46F1514F": { + "assume_role_policy": "\${data.aws_iam_policy_document.testECSService_ecs-task-assume_E7A7BA71.json}", + "name": "abides-dev-TaskExecutionRole", + "tags": { + "donnie": "throwinrockstonight", + "letsgo": "bowling" + } + }, + "testECSService_ecs-task-role_C04CE6DA": { + "assume_role_policy": "\${data.aws_iam_policy_document.testECSService_ecs-task-assume_E7A7BA71.json}", + "name": "abides-dev-TaskRole", + "tags": { + "donnie": "throwinrockstonight", + "letsgo": "bowling" + } + } + }, + "aws_iam_role_policy_attachment": { + "testECSService_ecs-task-execution-default-attachment_9FDF40D5": { + "policy_arn": "someArn", + "role": "\${aws_iam_role.testECSService_ecs-execution-role_46F1514F.id}" + } + } + } +}" +`; diff --git a/packages/terraform-modules/src/base/__snapshots__/ApplicationECSService.spec.ts.snap b/packages/terraform-modules/src/base/__snapshots__/ApplicationECSService.spec.ts.snap new file mode 100644 index 00000000..bdc9c1fd --- /dev/null +++ b/packages/terraform-modules/src/base/__snapshots__/ApplicationECSService.spec.ts.snap @@ -0,0 +1,1554 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ApplicationECSService attaches persistent (efs) storage to a ECS task 1`] = ` +"{ + "data": { + "aws_iam_policy_document": { + "testECSService_ecs-iam_ecs-task-assume_97906D9E": { + "statement": [ + { + "actions": [ + "sts:AssumeRole" + ], + "effect": "Allow", + "principals": [ + { + "identifiers": [ + "ecs-tasks.amazonaws.com" + ], + "type": "Service" + } + ] + } + ], + "version": "2012-10-17" + } + } + }, + "resource": { + "aws_cloudwatch_log_group": { + "testECSService_ecs-lebowski_226D2007": { + "name_prefix": "/ecs/abides-dev/lebowski", + "retention_in_days": 30 + } + }, + "aws_ecr_lifecycle_policy": { + "testECSService_ecr-lebowski_ecr-repo-lifecyclepolicy_19C9732F": { + "depends_on": [ + "aws_ecr_repository.testECSService_ecr-lebowski_ecr-repo_6D07DF86" + ], + "policy": "{\\"rules\\":[{\\"rulePriority\\":1,\\"description\\":\\"expire old images\\",\\"selection\\":{\\"tagStatus\\":\\"any\\",\\"countType\\":\\"imageCountMoreThan\\",\\"countNumber\\":800},\\"action\\":{\\"type\\":\\"expire\\"}}]}", + "repository": "\${aws_ecr_repository.testECSService_ecr-lebowski_ecr-repo_6D07DF86.name}" + } + }, + "aws_ecr_repository": { + "testECSService_ecr-lebowski_ecr-repo_6D07DF86": { + "image_scanning_configuration": { + "scan_on_push": true + }, + "name": "abides-dev-lebowski" + } + }, + "aws_ecs_service": { + "testECSService_ecs-service_33F309B3": { + "cluster": "gorp", + "depends_on": [ + "aws_ecr_repository.testECSService_ecr-lebowski_ecr-repo_6D07DF86" + ], + "deployment_controller": { + "type": "ECS" + }, + "deployment_maximum_percent": 200, + "deployment_minimum_healthy_percent": 100, + "desired_count": 2, + "launch_type": "FARGATE", + "lifecycle": { + "ignore_changes": [ + "desired_count", + "load_balancer" + ] + }, + "load_balancer": [ + ], + "name": "abides-dev", + "network_configuration": { + "security_groups": [ + "\${aws_security_group.testECSService_ecs_security_group_67979722.id}" + ], + "subnets": [ + "1.1.1.1", + "2.2.2.2" + ] + }, + "propagate_tags": "SERVICE", + "task_definition": "\${aws_ecs_task_definition.testECSService_ecs-task_A268C927.arn}" + } + }, + "aws_ecs_task_definition": { + "testECSService_ecs-task_A268C927": { + "container_definitions": "[{\\"dnsSearchDomains\\":null,\\"environmentFiles\\":null,\\"logConfiguration\\":{\\"logDriver\\":\\"awslogs\\",\\"secretOptions\\":[],\\"options\\":{\\"awslogs-group\\":\\"\${aws_cloudwatch_log_group.testECSService_ecs-lebowski_226D2007.name}\\",\\"awslogs-region\\":\\"us-east-1\\",\\"awslogs-stream-prefix\\":\\"ecs\\"}},\\"entryPoint\\":null,\\"portMappings\\":[],\\"linuxParameters\\":null,\\"cpu\\":0,\\"environment\\":[],\\"resourceRequirements\\":null,\\"ulimits\\":null,\\"repositoryCredentials\\":null,\\"dnsServers\\":null,\\"mountPoints\\":[{\\"containerPath\\":\\"/someMountPoint\\",\\"sourceVolume\\":\\"sourceVolume\\"}],\\"workingDirectory\\":null,\\"secrets\\":null,\\"dockerSecurityOptions\\":null,\\"memory\\":null,\\"memoryReservation\\":null,\\"volumesFrom\\":[],\\"stopTimeout\\":null,\\"image\\":\\"\${aws_ecr_repository.testECSService_ecr-lebowski_ecr-repo_6D07DF86.repository_url}:latest\\",\\"startTimeout\\":null,\\"firelensConfiguration\\":null,\\"dependsOn\\":null,\\"disableNetworking\\":null,\\"interactive\\":null,\\"healthCheck\\":null,\\"essential\\":true,\\"links\\":null,\\"hostname\\":null,\\"extraHosts\\":null,\\"pseudoTerminal\\":null,\\"user\\":null,\\"readonlyRootFilesystem\\":false,\\"dockerLabels\\":null,\\"systemControls\\":null,\\"privileged\\":null,\\"name\\":\\"lebowski\\"}]", + "cpu": "512", + "depends_on": [ + "aws_ecr_repository.testECSService_ecr-lebowski_ecr-repo_6D07DF86" + ], + "execution_role_arn": "\${aws_iam_role.testECSService_ecs-iam_ecs-execution-role_88EFEECE.arn}", + "family": "abides-dev", + "memory": "2048", + "network_mode": "awsvpc", + "requires_compatibilities": [ + "FARGATE" + ], + "task_role_arn": "\${aws_iam_role.testECSService_ecs-iam_ecs-task-role_83E10144.arn}", + "volume": [ + { + "efs_volume_configuration": { + "file_system_id": "someId" + }, + "name": "sourceVolume" + } + ] + } + }, + "aws_efs_file_system_policy": { + "testECSService_efsFsPolicy_AAD586DA": { + "depends_on": [ + "time_sleep.testECSService_waitTwoMinutes_F8D60FA5" + ], + "file_system_id": "someId", + "policy": "{\\"Version\\":\\"2012-10-17\\",\\"Id\\":\\"abides-dev\\",\\"Statement\\":[{\\"Sid\\":\\"abides-dev\\",\\"Effect\\":\\"Allow\\",\\"Principal\\":{\\"AWS\\":\\"*\\"},\\"Resource\\":\\"fakeArn\\",\\"Action\\":[\\"elasticfilesystem:ClientMount\\",\\"elasticfilesystem:ClientWrite\\",\\"elasticfilesystem:ClientRootAccess\\"],\\"Condition\\":{\\"Bool\\":{\\"elasticfilesystem:AccessedViaMountTarget\\":\\"true\\"}}}]}" + } + }, + "aws_efs_mount_target": { + "testECSService_efs_mount_target_C9F488D3": { + "file_system_id": "someId", + "for_each": "\${toset([\\"1.1.1.1\\", \\"2.2.2.2\\"])}", + "security_groups": [ + "\${aws_security_group.testECSService_efs_mount_sg_BE274D85.id}" + ], + "subnet_id": "\${each.value}" + } + }, + "aws_iam_role": { + "testECSService_ecs-iam_ecs-execution-role_88EFEECE": { + "assume_role_policy": "\${data.aws_iam_policy_document.testECSService_ecs-iam_ecs-task-assume_97906D9E.json}", + "name": "abides-dev-TaskExecutionRole" + }, + "testECSService_ecs-iam_ecs-task-role_83E10144": { + "assume_role_policy": "\${data.aws_iam_policy_document.testECSService_ecs-iam_ecs-task-assume_97906D9E.json}", + "name": "abides-dev-TaskRole" + } + }, + "aws_iam_role_policy_attachment": { + "testECSService_ecs-iam_ecs-task-execution-default-attachment_7AC36BF7": { + "policy_arn": "someArn", + "role": "\${aws_iam_role.testECSService_ecs-iam_ecs-execution-role_88EFEECE.id}" + } + }, + "aws_security_group": { + "testECSService_ecs_security_group_67979722": { + "description": "Internal ECS Security Group (Managed by Terraform)", + "egress": [ + { + "cidr_blocks": [ + "0.0.0.0/0" + ], + "description": "required", + "from_port": 0, + "ipv6_cidr_blocks": [ + ], + "prefix_list_ids": [ + ], + "protocol": "-1", + "security_groups": [ + ], + "self": null, + "to_port": 0 + } + ], + "ingress": [ + ], + "lifecycle": { + "create_before_destroy": true + }, + "name_prefix": "abides-dev-ECSSecurityGroup", + "vpc_id": "myhouse" + }, + "testECSService_efs_mount_sg_BE274D85": { + "description": "ECS EFS Mount (Managed by Terraform)", + "egress": [ + { + "cidr_blocks": [ + "0.0.0.0/0" + ], + "description": "required", + "from_port": 0, + "ipv6_cidr_blocks": [ + ], + "prefix_list_ids": [ + ], + "protocol": "-1", + "security_groups": [ + ], + "self": null, + "to_port": 0 + } + ], + "ingress": [ + { + "cidr_blocks": [ + ], + "description": "required", + "from_port": 2049, + "ipv6_cidr_blocks": [ + ], + "prefix_list_ids": [ + ], + "protocol": "TCP", + "security_groups": [ + "\${aws_security_group.testECSService_ecs_security_group_67979722.id}" + ], + "self": null, + "to_port": 2049 + } + ], + "lifecycle": { + "create_before_destroy": true + }, + "name_prefix": "abides-dev-ECSSMountPoint", + "vpc_id": "myhouse" + } + }, + "time_sleep": { + "testECSService_waitTwoMinutes_F8D60FA5": { + "create_duration": "2m", + "depends_on": [ + ] + } + } + } +}" +`; + +exports[`ApplicationECSService renders an ECS service with code deploy 1`] = ` +"{ + "data": { + "aws_iam_policy_document": { + "testECSService_ecs-iam_ecs-task-assume_97906D9E": { + "statement": [ + { + "actions": [ + "sts:AssumeRole" + ], + "effect": "Allow", + "principals": [ + { + "identifiers": [ + "ecs-tasks.amazonaws.com" + ], + "type": "Service" + } + ] + } + ], + "version": "2012-10-17" + } + } + }, + "resource": { + "aws_ecs_service": { + "testECSService_ecs-service_33F309B3": { + "cluster": "gorp", + "depends_on": [ + ], + "deployment_controller": { + "type": "CODE_DEPLOY" + }, + "deployment_maximum_percent": 200, + "deployment_minimum_healthy_percent": 100, + "desired_count": 2, + "launch_type": "FARGATE", + "lifecycle": { + "ignore_changes": [ + "desired_count", + "load_balancer", + "task_definition" + ] + }, + "load_balancer": [ + ], + "name": "abides-dev", + "network_configuration": { + "security_groups": [ + "\${aws_security_group.testECSService_ecs_security_group_67979722.id}" + ], + "subnets": [ + "1.1.1.1", + "2.2.2.2" + ] + }, + "propagate_tags": "SERVICE", + "task_definition": "\${aws_ecs_task_definition.testECSService_ecs-task_A268C927.arn}" + } + }, + "aws_ecs_task_definition": { + "testECSService_ecs-task_A268C927": { + "container_definitions": "[]", + "cpu": "512", + "depends_on": [ + ], + "execution_role_arn": "\${aws_iam_role.testECSService_ecs-iam_ecs-execution-role_88EFEECE.arn}", + "family": "abides-dev", + "memory": "2048", + "network_mode": "awsvpc", + "requires_compatibilities": [ + "FARGATE" + ], + "task_role_arn": "\${aws_iam_role.testECSService_ecs-iam_ecs-task-role_83E10144.arn}", + "volume": [ + ] + } + }, + "aws_iam_role": { + "testECSService_ecs-iam_ecs-execution-role_88EFEECE": { + "assume_role_policy": "\${data.aws_iam_policy_document.testECSService_ecs-iam_ecs-task-assume_97906D9E.json}", + "name": "abides-dev-TaskExecutionRole" + }, + "testECSService_ecs-iam_ecs-task-role_83E10144": { + "assume_role_policy": "\${data.aws_iam_policy_document.testECSService_ecs-iam_ecs-task-assume_97906D9E.json}", + "name": "abides-dev-TaskRole" + } + }, + "aws_iam_role_policy_attachment": { + "testECSService_ecs-iam_ecs-task-execution-default-attachment_7AC36BF7": { + "policy_arn": "someArn", + "role": "\${aws_iam_role.testECSService_ecs-iam_ecs-execution-role_88EFEECE.id}" + } + }, + "aws_security_group": { + "testECSService_ecs_security_group_67979722": { + "description": "Internal ECS Security Group (Managed by Terraform)", + "egress": [ + { + "cidr_blocks": [ + "0.0.0.0/0" + ], + "description": "required", + "from_port": 0, + "ipv6_cidr_blocks": [ + ], + "prefix_list_ids": [ + ], + "protocol": "-1", + "security_groups": [ + ], + "self": null, + "to_port": 0 + } + ], + "ingress": [ + ], + "lifecycle": { + "create_before_destroy": true + }, + "name_prefix": "abides-dev-ECSSecurityGroup", + "vpc_id": "myhouse" + } + } + } +}" +`; + +exports[`ApplicationECSService renders an ECS service with code deploy and excludes the code deployment command resource when useCodePipeline is true 1`] = ` +"{ + "data": { + "aws_iam_policy_document": { + "testECSService_ecs-iam_ecs-task-assume_97906D9E": { + "statement": [ + { + "actions": [ + "sts:AssumeRole" + ], + "effect": "Allow", + "principals": [ + { + "identifiers": [ + "ecs-tasks.amazonaws.com" + ], + "type": "Service" + } + ] + } + ], + "version": "2012-10-17" + } + } + }, + "resource": { + "aws_ecs_service": { + "testECSService_ecs-service_33F309B3": { + "cluster": "gorp", + "depends_on": [ + ], + "deployment_controller": { + "type": "CODE_DEPLOY" + }, + "deployment_maximum_percent": 200, + "deployment_minimum_healthy_percent": 100, + "desired_count": 2, + "launch_type": "FARGATE", + "lifecycle": { + "ignore_changes": [ + "desired_count", + "load_balancer", + "task_definition" + ] + }, + "load_balancer": [ + ], + "name": "abides-dev", + "network_configuration": { + "security_groups": [ + "\${aws_security_group.testECSService_ecs_security_group_67979722.id}" + ], + "subnets": [ + "1.1.1.1", + "2.2.2.2" + ] + }, + "propagate_tags": "SERVICE", + "task_definition": "\${aws_ecs_task_definition.testECSService_ecs-task_A268C927.arn}" + } + }, + "aws_ecs_task_definition": { + "testECSService_ecs-task_A268C927": { + "container_definitions": "[]", + "cpu": "512", + "depends_on": [ + ], + "execution_role_arn": "\${aws_iam_role.testECSService_ecs-iam_ecs-execution-role_88EFEECE.arn}", + "family": "abides-dev", + "memory": "2048", + "network_mode": "awsvpc", + "requires_compatibilities": [ + "FARGATE" + ], + "task_role_arn": "\${aws_iam_role.testECSService_ecs-iam_ecs-task-role_83E10144.arn}", + "volume": [ + ] + } + }, + "aws_iam_role": { + "testECSService_ecs-iam_ecs-execution-role_88EFEECE": { + "assume_role_policy": "\${data.aws_iam_policy_document.testECSService_ecs-iam_ecs-task-assume_97906D9E.json}", + "name": "abides-dev-TaskExecutionRole" + }, + "testECSService_ecs-iam_ecs-task-role_83E10144": { + "assume_role_policy": "\${data.aws_iam_policy_document.testECSService_ecs-iam_ecs-task-assume_97906D9E.json}", + "name": "abides-dev-TaskRole" + } + }, + "aws_iam_role_policy_attachment": { + "testECSService_ecs-iam_ecs-task-execution-default-attachment_7AC36BF7": { + "policy_arn": "someArn", + "role": "\${aws_iam_role.testECSService_ecs-iam_ecs-execution-role_88EFEECE.id}" + } + }, + "aws_security_group": { + "testECSService_ecs_security_group_67979722": { + "description": "Internal ECS Security Group (Managed by Terraform)", + "egress": [ + { + "cidr_blocks": [ + "0.0.0.0/0" + ], + "description": "required", + "from_port": 0, + "ipv6_cidr_blocks": [ + ], + "prefix_list_ids": [ + ], + "protocol": "-1", + "security_groups": [ + ], + "self": null, + "to_port": 0 + } + ], + "ingress": [ + ], + "lifecycle": { + "create_before_destroy": true + }, + "name_prefix": "abides-dev-ECSSecurityGroup", + "vpc_id": "myhouse" + } + } + } +}" +`; + +exports[`ApplicationECSService renders an ECS service with code deploy notifications set for failed only 1`] = ` +"{ + "data": { + "aws_iam_policy_document": { + "testECSService_ecs-iam_ecs-task-assume_97906D9E": { + "statement": [ + { + "actions": [ + "sts:AssumeRole" + ], + "effect": "Allow", + "principals": [ + { + "identifiers": [ + "ecs-tasks.amazonaws.com" + ], + "type": "Service" + } + ] + } + ], + "version": "2012-10-17" + } + } + }, + "resource": { + "aws_ecs_service": { + "testECSService_ecs-service_33F309B3": { + "cluster": "gorp", + "depends_on": [ + ], + "deployment_controller": { + "type": "CODE_DEPLOY" + }, + "deployment_maximum_percent": 200, + "deployment_minimum_healthy_percent": 100, + "desired_count": 2, + "launch_type": "FARGATE", + "lifecycle": { + "ignore_changes": [ + "desired_count", + "load_balancer", + "task_definition" + ] + }, + "load_balancer": [ + ], + "name": "abides-dev", + "network_configuration": { + "security_groups": [ + "\${aws_security_group.testECSService_ecs_security_group_67979722.id}" + ], + "subnets": [ + "1.1.1.1", + "2.2.2.2" + ] + }, + "propagate_tags": "SERVICE", + "task_definition": "\${aws_ecs_task_definition.testECSService_ecs-task_A268C927.arn}" + } + }, + "aws_ecs_task_definition": { + "testECSService_ecs-task_A268C927": { + "container_definitions": "[]", + "cpu": "512", + "depends_on": [ + ], + "execution_role_arn": "\${aws_iam_role.testECSService_ecs-iam_ecs-execution-role_88EFEECE.arn}", + "family": "abides-dev", + "memory": "2048", + "network_mode": "awsvpc", + "requires_compatibilities": [ + "FARGATE" + ], + "task_role_arn": "\${aws_iam_role.testECSService_ecs-iam_ecs-task-role_83E10144.arn}", + "volume": [ + ] + } + }, + "aws_iam_role": { + "testECSService_ecs-iam_ecs-execution-role_88EFEECE": { + "assume_role_policy": "\${data.aws_iam_policy_document.testECSService_ecs-iam_ecs-task-assume_97906D9E.json}", + "name": "abides-dev-TaskExecutionRole" + }, + "testECSService_ecs-iam_ecs-task-role_83E10144": { + "assume_role_policy": "\${data.aws_iam_policy_document.testECSService_ecs-iam_ecs-task-assume_97906D9E.json}", + "name": "abides-dev-TaskRole" + } + }, + "aws_iam_role_policy_attachment": { + "testECSService_ecs-iam_ecs-task-execution-default-attachment_7AC36BF7": { + "policy_arn": "someArn", + "role": "\${aws_iam_role.testECSService_ecs-iam_ecs-execution-role_88EFEECE.id}" + } + }, + "aws_security_group": { + "testECSService_ecs_security_group_67979722": { + "description": "Internal ECS Security Group (Managed by Terraform)", + "egress": [ + { + "cidr_blocks": [ + "0.0.0.0/0" + ], + "description": "required", + "from_port": 0, + "ipv6_cidr_blocks": [ + ], + "prefix_list_ids": [ + ], + "protocol": "-1", + "security_groups": [ + ], + "self": null, + "to_port": 0 + } + ], + "ingress": [ + ], + "lifecycle": { + "create_before_destroy": true + }, + "name_prefix": "abides-dev-ECSSecurityGroup", + "vpc_id": "myhouse" + } + } + } +}" +`; + +exports[`ApplicationECSService renders an ECS service with full container definition props 1`] = ` +"{ + "data": { + "aws_iam_policy_document": { + "testECSService_ecs-iam_ecs-task-assume_97906D9E": { + "statement": [ + { + "actions": [ + "sts:AssumeRole" + ], + "effect": "Allow", + "principals": [ + { + "identifiers": [ + "ecs-tasks.amazonaws.com" + ], + "type": "Service" + } + ] + } + ], + "version": "2012-10-17" + } + } + }, + "resource": { + "aws_ecs_service": { + "testECSService_ecs-service_33F309B3": { + "cluster": "gorp", + "depends_on": [ + ], + "deployment_controller": { + "type": "ECS" + }, + "deployment_maximum_percent": 400, + "deployment_minimum_healthy_percent": 80, + "desired_count": 4, + "launch_type": "ROCKET", + "lifecycle": { + "ignore_changes": [ + "bowling", + "donnie", + "autobahn" + ] + }, + "load_balancer": [ + ], + "name": "abides-dev", + "network_configuration": { + "security_groups": [ + "\${aws_security_group.testECSService_ecs_security_group_67979722.id}" + ], + "subnets": [ + "1.1.1.1", + "2.2.2.2" + ] + }, + "propagate_tags": "SERVICE", + "task_definition": "\${aws_ecs_task_definition.testECSService_ecs-task_A268C927.arn}" + } + }, + "aws_ecs_task_definition": { + "testECSService_ecs-task_A268C927": { + "container_definitions": "[{\\"dnsSearchDomains\\":null,\\"environmentFiles\\":null,\\"logConfiguration\\":{\\"logDriver\\":\\"awslogs\\",\\"secretOptions\\":[],\\"options\\":{\\"awslogs-group\\":\\"test/log/group\\",\\"awslogs-region\\":\\"us-east-1\\",\\"awslogs-stream-prefix\\":\\"ecs\\"}},\\"entryPoint\\":null,\\"portMappings\\":[{\\"containerPort\\":3002,\\"hostPort\\":3001}],\\"linuxParameters\\":null,\\"cpu\\":0,\\"environment\\":[{\\"name\\":\\"rug\\",\\"value\\":\\"tiedtheroomtogether\\"}],\\"resourceRequirements\\":null,\\"ulimits\\":null,\\"repositoryCredentials\\":{\\"credentialsParameter\\":\\"someArn\\"},\\"dnsServers\\":null,\\"mountPoints\\":[],\\"workingDirectory\\":null,\\"secrets\\":[{\\"name\\":\\"donnie\\",\\"valueFrom\\":\\"throwinrockstonight\\"}],\\"dockerSecurityOptions\\":null,\\"memory\\":null,\\"memoryReservation\\":null,\\"volumesFrom\\":[],\\"stopTimeout\\":null,\\"image\\":\\"beverage-here/0.1\\",\\"startTimeout\\":null,\\"firelensConfiguration\\":null,\\"dependsOn\\":null,\\"disableNetworking\\":null,\\"interactive\\":null,\\"healthCheck\\":null,\\"essential\\":true,\\"links\\":null,\\"hostname\\":null,\\"extraHosts\\":null,\\"pseudoTerminal\\":null,\\"user\\":null,\\"readonlyRootFilesystem\\":false,\\"dockerLabels\\":null,\\"systemControls\\":null,\\"privileged\\":null,\\"name\\":\\"lebowski\\"}]", + "cpu": "512", + "depends_on": [ + ], + "execution_role_arn": "\${aws_iam_role.testECSService_ecs-iam_ecs-execution-role_88EFEECE.arn}", + "family": "abides-dev", + "memory": "2048", + "network_mode": "awsvpc", + "requires_compatibilities": [ + "FARGATE" + ], + "task_role_arn": "\${aws_iam_role.testECSService_ecs-iam_ecs-task-role_83E10144.arn}", + "volume": [ + ] + } + }, + "aws_iam_role": { + "testECSService_ecs-iam_ecs-execution-role_88EFEECE": { + "assume_role_policy": "\${data.aws_iam_policy_document.testECSService_ecs-iam_ecs-task-assume_97906D9E.json}", + "name": "abides-dev-TaskExecutionRole" + }, + "testECSService_ecs-iam_ecs-task-role_83E10144": { + "assume_role_policy": "\${data.aws_iam_policy_document.testECSService_ecs-iam_ecs-task-assume_97906D9E.json}", + "name": "abides-dev-TaskRole" + } + }, + "aws_iam_role_policy_attachment": { + "testECSService_ecs-iam_ecs-task-execution-default-attachment_7AC36BF7": { + "policy_arn": "someArn", + "role": "\${aws_iam_role.testECSService_ecs-iam_ecs-execution-role_88EFEECE.id}" + } + }, + "aws_security_group": { + "testECSService_ecs_security_group_67979722": { + "description": "Internal ECS Security Group (Managed by Terraform)", + "egress": [ + { + "cidr_blocks": [ + "0.0.0.0/0" + ], + "description": "required", + "from_port": 0, + "ipv6_cidr_blocks": [ + ], + "prefix_list_ids": [ + ], + "protocol": "-1", + "security_groups": [ + ], + "self": null, + "to_port": 0 + } + ], + "ingress": [ + ], + "lifecycle": { + "create_before_destroy": true + }, + "name_prefix": "abides-dev-ECSSecurityGroup", + "vpc_id": "myhouse" + } + } + } +}" +`; + +exports[`ApplicationECSService renders an ECS service with full container definition props and ALB security group config 1`] = ` +"{ + "data": { + "aws_iam_policy_document": { + "testECSService_ecs-iam_ecs-task-assume_97906D9E": { + "statement": [ + { + "actions": [ + "sts:AssumeRole" + ], + "effect": "Allow", + "principals": [ + { + "identifiers": [ + "ecs-tasks.amazonaws.com" + ], + "type": "Service" + } + ] + } + ], + "version": "2012-10-17" + } + } + }, + "resource": { + "aws_alb_listener_rule": { + "testECSService_listener_rule_A3A4BB9D": { + "action": [ + { + "target_group_arn": "\${aws_alb_target_group.testECSService_blue_target_group_ecs_target_group_AB8F309D.arn}", + "type": "forward" + } + ], + "condition": [ + { + "path_pattern": { + "values": [ + "*" + ] + } + } + ], + "lifecycle": { + "ignore_changes": [ + "action" + ] + }, + "listener_arn": "listen-to-me", + "priority": 1 + } + }, + "aws_alb_target_group": { + "testECSService_blue_target_group_ecs_target_group_AB8F309D": { + "deregistration_delay": "120", + "health_check": { + "healthy_threshold": 5, + "interval": 15, + "path": "/health", + "protocol": "HTTP", + "unhealthy_threshold": 3 + }, + "name_prefix": "shortb", + "port": 80, + "protocol": "HTTP", + "tags": { + "type": "blue" + }, + "target_type": "ip", + "vpc_id": "myhouse" + } + }, + "aws_ecs_service": { + "testECSService_ecs-service_33F309B3": { + "cluster": "gorp", + "depends_on": [ + "aws_alb_target_group.testECSService_blue_target_group_ecs_target_group_AB8F309D", + "aws_alb_listener_rule.testECSService_listener_rule_A3A4BB9D" + ], + "deployment_controller": { + "type": "ECS" + }, + "deployment_maximum_percent": 200, + "deployment_minimum_healthy_percent": 100, + "desired_count": 2, + "launch_type": "FARGATE", + "lifecycle": { + "ignore_changes": [ + "desired_count", + "load_balancer" + ] + }, + "load_balancer": [ + { + "container_name": "runme", + "container_port": 3000, + "target_group_arn": "\${aws_alb_target_group.testECSService_blue_target_group_ecs_target_group_AB8F309D.arn}" + } + ], + "name": "abides-dev", + "network_configuration": { + "security_groups": [ + "\${aws_security_group.testECSService_ecs_security_group_67979722.id}" + ], + "subnets": [ + "1.1.1.1", + "2.2.2.2" + ] + }, + "propagate_tags": "SERVICE", + "task_definition": "\${aws_ecs_task_definition.testECSService_ecs-task_A268C927.arn}" + } + }, + "aws_ecs_task_definition": { + "testECSService_ecs-task_A268C927": { + "container_definitions": "[{\\"dnsSearchDomains\\":null,\\"environmentFiles\\":null,\\"logConfiguration\\":{\\"logDriver\\":\\"awslogs\\",\\"secretOptions\\":[],\\"options\\":{\\"awslogs-group\\":\\"test/log/group\\",\\"awslogs-region\\":\\"us-east-1\\",\\"awslogs-stream-prefix\\":\\"ecs\\"}},\\"entryPoint\\":null,\\"portMappings\\":[{\\"containerPort\\":3002,\\"hostPort\\":3001}],\\"linuxParameters\\":null,\\"cpu\\":0,\\"environment\\":[{\\"name\\":\\"rug\\",\\"value\\":\\"tiedtheroomtogether\\"}],\\"resourceRequirements\\":null,\\"ulimits\\":null,\\"repositoryCredentials\\":{\\"credentialsParameter\\":\\"someArn\\"},\\"dnsServers\\":null,\\"mountPoints\\":[],\\"workingDirectory\\":null,\\"secrets\\":[{\\"name\\":\\"donnie\\",\\"valueFrom\\":\\"throwinrockstonight\\"}],\\"dockerSecurityOptions\\":null,\\"memory\\":null,\\"memoryReservation\\":null,\\"volumesFrom\\":[],\\"stopTimeout\\":null,\\"image\\":\\"beverage-here/0.1\\",\\"startTimeout\\":null,\\"firelensConfiguration\\":null,\\"dependsOn\\":null,\\"disableNetworking\\":null,\\"interactive\\":null,\\"healthCheck\\":null,\\"essential\\":true,\\"links\\":null,\\"hostname\\":null,\\"extraHosts\\":null,\\"pseudoTerminal\\":null,\\"user\\":null,\\"readonlyRootFilesystem\\":false,\\"dockerLabels\\":null,\\"systemControls\\":null,\\"privileged\\":null,\\"name\\":\\"lebowski\\"}]", + "cpu": "512", + "depends_on": [ + ], + "execution_role_arn": "\${aws_iam_role.testECSService_ecs-iam_ecs-execution-role_88EFEECE.arn}", + "family": "abides-dev", + "memory": "2048", + "network_mode": "awsvpc", + "requires_compatibilities": [ + "FARGATE" + ], + "task_role_arn": "\${aws_iam_role.testECSService_ecs-iam_ecs-task-role_83E10144.arn}", + "volume": [ + ] + } + }, + "aws_iam_role": { + "testECSService_ecs-iam_ecs-execution-role_88EFEECE": { + "assume_role_policy": "\${data.aws_iam_policy_document.testECSService_ecs-iam_ecs-task-assume_97906D9E.json}", + "name": "abides-dev-TaskExecutionRole" + }, + "testECSService_ecs-iam_ecs-task-role_83E10144": { + "assume_role_policy": "\${data.aws_iam_policy_document.testECSService_ecs-iam_ecs-task-assume_97906D9E.json}", + "name": "abides-dev-TaskRole" + } + }, + "aws_iam_role_policy_attachment": { + "testECSService_ecs-iam_ecs-task-execution-default-attachment_7AC36BF7": { + "policy_arn": "someArn", + "role": "\${aws_iam_role.testECSService_ecs-iam_ecs-execution-role_88EFEECE.id}" + } + }, + "aws_security_group": { + "testECSService_ecs_security_group_67979722": { + "description": "Internal ECS Security Group (Managed by Terraform)", + "egress": [ + { + "cidr_blocks": [ + "0.0.0.0/0" + ], + "description": "required", + "from_port": 0, + "ipv6_cidr_blocks": [ + ], + "prefix_list_ids": [ + ], + "protocol": "-1", + "security_groups": [ + ], + "self": null, + "to_port": 0 + } + ], + "ingress": [ + { + "cidr_blocks": [ + ], + "description": "required", + "from_port": 80, + "ipv6_cidr_blocks": [ + ], + "prefix_list_ids": [ + ], + "protocol": "TCP", + "security_groups": [ + "strike" + ], + "self": null, + "to_port": 3000 + } + ], + "lifecycle": { + "create_before_destroy": true + }, + "name_prefix": "abides-dev-ECSSecurityGroup", + "vpc_id": "myhouse" + } + } + } +}" +`; + +exports[`ApplicationECSService renders an ECS service with minimal config 1`] = ` +"{ + "data": { + "aws_iam_policy_document": { + "testECSService_ecs-iam_ecs-task-assume_97906D9E": { + "statement": [ + { + "actions": [ + "sts:AssumeRole" + ], + "effect": "Allow", + "principals": [ + { + "identifiers": [ + "ecs-tasks.amazonaws.com" + ], + "type": "Service" + } + ] + } + ], + "version": "2012-10-17" + } + } + }, + "resource": { + "aws_ecs_service": { + "testECSService_ecs-service_33F309B3": { + "cluster": "gorp", + "depends_on": [ + ], + "deployment_controller": { + "type": "ECS" + }, + "deployment_maximum_percent": 200, + "deployment_minimum_healthy_percent": 100, + "desired_count": 2, + "launch_type": "FARGATE", + "lifecycle": { + "ignore_changes": [ + "desired_count", + "load_balancer" + ] + }, + "load_balancer": [ + ], + "name": "abides-dev", + "network_configuration": { + "security_groups": [ + "\${aws_security_group.testECSService_ecs_security_group_67979722.id}" + ], + "subnets": [ + "1.1.1.1", + "2.2.2.2" + ] + }, + "propagate_tags": "SERVICE", + "task_definition": "\${aws_ecs_task_definition.testECSService_ecs-task_A268C927.arn}" + } + }, + "aws_ecs_task_definition": { + "testECSService_ecs-task_A268C927": { + "container_definitions": "[]", + "cpu": "512", + "depends_on": [ + ], + "execution_role_arn": "\${aws_iam_role.testECSService_ecs-iam_ecs-execution-role_88EFEECE.arn}", + "family": "abides-dev", + "memory": "2048", + "network_mode": "awsvpc", + "requires_compatibilities": [ + "FARGATE" + ], + "task_role_arn": "\${aws_iam_role.testECSService_ecs-iam_ecs-task-role_83E10144.arn}", + "volume": [ + ] + } + }, + "aws_iam_role": { + "testECSService_ecs-iam_ecs-execution-role_88EFEECE": { + "assume_role_policy": "\${data.aws_iam_policy_document.testECSService_ecs-iam_ecs-task-assume_97906D9E.json}", + "name": "abides-dev-TaskExecutionRole" + }, + "testECSService_ecs-iam_ecs-task-role_83E10144": { + "assume_role_policy": "\${data.aws_iam_policy_document.testECSService_ecs-iam_ecs-task-assume_97906D9E.json}", + "name": "abides-dev-TaskRole" + } + }, + "aws_iam_role_policy_attachment": { + "testECSService_ecs-iam_ecs-task-execution-default-attachment_7AC36BF7": { + "policy_arn": "someArn", + "role": "\${aws_iam_role.testECSService_ecs-iam_ecs-execution-role_88EFEECE.id}" + } + }, + "aws_security_group": { + "testECSService_ecs_security_group_67979722": { + "description": "Internal ECS Security Group (Managed by Terraform)", + "egress": [ + { + "cidr_blocks": [ + "0.0.0.0/0" + ], + "description": "required", + "from_port": 0, + "ipv6_cidr_blocks": [ + ], + "prefix_list_ids": [ + ], + "protocol": "-1", + "security_groups": [ + ], + "self": null, + "to_port": 0 + } + ], + "ingress": [ + ], + "lifecycle": { + "create_before_destroy": true + }, + "name_prefix": "abides-dev-ECSSecurityGroup", + "vpc_id": "myhouse" + } + } + } +}" +`; + +exports[`ApplicationECSService renders an ECS service with mountPoints deduplicated in the task definition 1`] = ` +"{ + "data": { + "aws_iam_policy_document": { + "testECSService_ecs-iam_ecs-task-assume_97906D9E": { + "statement": [ + { + "actions": [ + "sts:AssumeRole" + ], + "effect": "Allow", + "principals": [ + { + "identifiers": [ + "ecs-tasks.amazonaws.com" + ], + "type": "Service" + } + ] + } + ], + "version": "2012-10-17" + } + } + }, + "resource": { + "aws_cloudwatch_log_group": { + "testECSService_ecs-container1_FA0D3F88": { + "name_prefix": "/ecs/abides-dev/container1", + "retention_in_days": 30 + }, + "testECSService_ecs-container2_E4AB2284": { + "name_prefix": "/ecs/abides-dev/container2", + "retention_in_days": 30 + }, + "testECSService_ecs-container3_1286AAA3": { + "name_prefix": "/ecs/abides-dev/container3", + "retention_in_days": 30 + } + }, + "aws_ecr_lifecycle_policy": { + "testECSService_ecr-container1_ecr-repo-lifecyclepolicy_FAA9164A": { + "depends_on": [ + "aws_ecr_repository.testECSService_ecr-container1_ecr-repo_6BB90A2E" + ], + "policy": "{\\"rules\\":[{\\"rulePriority\\":1,\\"description\\":\\"expire old images\\",\\"selection\\":{\\"tagStatus\\":\\"any\\",\\"countType\\":\\"imageCountMoreThan\\",\\"countNumber\\":800},\\"action\\":{\\"type\\":\\"expire\\"}}]}", + "repository": "\${aws_ecr_repository.testECSService_ecr-container1_ecr-repo_6BB90A2E.name}" + }, + "testECSService_ecr-container2_ecr-repo-lifecyclepolicy_326A08A8": { + "depends_on": [ + "aws_ecr_repository.testECSService_ecr-container2_ecr-repo_6980E3F0" + ], + "policy": "{\\"rules\\":[{\\"rulePriority\\":1,\\"description\\":\\"expire old images\\",\\"selection\\":{\\"tagStatus\\":\\"any\\",\\"countType\\":\\"imageCountMoreThan\\",\\"countNumber\\":800},\\"action\\":{\\"type\\":\\"expire\\"}}]}", + "repository": "\${aws_ecr_repository.testECSService_ecr-container2_ecr-repo_6980E3F0.name}" + }, + "testECSService_ecr-container3_ecr-repo-lifecyclepolicy_C3FDC63F": { + "depends_on": [ + "aws_ecr_repository.testECSService_ecr-container3_ecr-repo_F25059FD" + ], + "policy": "{\\"rules\\":[{\\"rulePriority\\":1,\\"description\\":\\"expire old images\\",\\"selection\\":{\\"tagStatus\\":\\"any\\",\\"countType\\":\\"imageCountMoreThan\\",\\"countNumber\\":800},\\"action\\":{\\"type\\":\\"expire\\"}}]}", + "repository": "\${aws_ecr_repository.testECSService_ecr-container3_ecr-repo_F25059FD.name}" + } + }, + "aws_ecr_repository": { + "testECSService_ecr-container1_ecr-repo_6BB90A2E": { + "image_scanning_configuration": { + "scan_on_push": true + }, + "name": "abides-dev-container1" + }, + "testECSService_ecr-container2_ecr-repo_6980E3F0": { + "image_scanning_configuration": { + "scan_on_push": true + }, + "name": "abides-dev-container2" + }, + "testECSService_ecr-container3_ecr-repo_F25059FD": { + "image_scanning_configuration": { + "scan_on_push": true + }, + "name": "abides-dev-container3" + } + }, + "aws_ecs_service": { + "testECSService_ecs-service_33F309B3": { + "cluster": "gorp", + "depends_on": [ + "aws_ecr_repository.testECSService_ecr-container1_ecr-repo_6BB90A2E", + "aws_ecr_repository.testECSService_ecr-container2_ecr-repo_6980E3F0", + "aws_ecr_repository.testECSService_ecr-container3_ecr-repo_F25059FD" + ], + "deployment_controller": { + "type": "ECS" + }, + "deployment_maximum_percent": 200, + "deployment_minimum_healthy_percent": 100, + "desired_count": 2, + "launch_type": "ROCKET", + "lifecycle": { + "ignore_changes": [ + "desired_count", + "load_balancer" + ] + }, + "load_balancer": [ + ], + "name": "abides-dev", + "network_configuration": { + "security_groups": [ + "\${aws_security_group.testECSService_ecs_security_group_67979722.id}" + ], + "subnets": [ + "1.1.1.1", + "2.2.2.2" + ] + }, + "propagate_tags": "SERVICE", + "task_definition": "\${aws_ecs_task_definition.testECSService_ecs-task_A268C927.arn}" + } + }, + "aws_ecs_task_definition": { + "testECSService_ecs-task_A268C927": { + "container_definitions": "[{\\"dnsSearchDomains\\":null,\\"environmentFiles\\":null,\\"logConfiguration\\":{\\"logDriver\\":\\"awslogs\\",\\"secretOptions\\":[],\\"options\\":{\\"awslogs-group\\":\\"\${aws_cloudwatch_log_group.testECSService_ecs-container1_FA0D3F88.name}\\",\\"awslogs-region\\":\\"us-east-1\\",\\"awslogs-stream-prefix\\":\\"ecs\\"}},\\"entryPoint\\":null,\\"portMappings\\":[],\\"linuxParameters\\":null,\\"cpu\\":0,\\"environment\\":[],\\"resourceRequirements\\":null,\\"ulimits\\":null,\\"repositoryCredentials\\":null,\\"dnsServers\\":null,\\"mountPoints\\":[],\\"workingDirectory\\":null,\\"secrets\\":null,\\"dockerSecurityOptions\\":null,\\"memory\\":null,\\"memoryReservation\\":null,\\"volumesFrom\\":[],\\"stopTimeout\\":null,\\"image\\":\\"\${aws_ecr_repository.testECSService_ecr-container1_ecr-repo_6BB90A2E.repository_url}:latest\\",\\"startTimeout\\":null,\\"firelensConfiguration\\":null,\\"dependsOn\\":null,\\"disableNetworking\\":null,\\"interactive\\":null,\\"healthCheck\\":null,\\"essential\\":true,\\"links\\":null,\\"hostname\\":null,\\"extraHosts\\":null,\\"pseudoTerminal\\":null,\\"user\\":null,\\"readonlyRootFilesystem\\":false,\\"dockerLabels\\":null,\\"systemControls\\":null,\\"privileged\\":null,\\"name\\":\\"container1\\"},{\\"dnsSearchDomains\\":null,\\"environmentFiles\\":null,\\"logConfiguration\\":{\\"logDriver\\":\\"awslogs\\",\\"secretOptions\\":[],\\"options\\":{\\"awslogs-group\\":\\"\${aws_cloudwatch_log_group.testECSService_ecs-container2_E4AB2284.name}\\",\\"awslogs-region\\":\\"us-east-1\\",\\"awslogs-stream-prefix\\":\\"ecs\\"}},\\"entryPoint\\":null,\\"portMappings\\":[],\\"linuxParameters\\":null,\\"cpu\\":0,\\"environment\\":[],\\"resourceRequirements\\":null,\\"ulimits\\":null,\\"repositoryCredentials\\":null,\\"dnsServers\\":null,\\"mountPoints\\":[{\\"sourceVolume\\":\\"src1\\",\\"containerPath\\":\\"/src1\\"},{\\"sourceVolume\\":\\"src2\\",\\"containerPath\\":\\"/src2\\"}],\\"workingDirectory\\":null,\\"secrets\\":null,\\"dockerSecurityOptions\\":null,\\"memory\\":null,\\"memoryReservation\\":null,\\"volumesFrom\\":[],\\"stopTimeout\\":null,\\"image\\":\\"\${aws_ecr_repository.testECSService_ecr-container2_ecr-repo_6980E3F0.repository_url}:latest\\",\\"startTimeout\\":null,\\"firelensConfiguration\\":null,\\"dependsOn\\":null,\\"disableNetworking\\":null,\\"interactive\\":null,\\"healthCheck\\":null,\\"essential\\":true,\\"links\\":null,\\"hostname\\":null,\\"extraHosts\\":null,\\"pseudoTerminal\\":null,\\"user\\":null,\\"readonlyRootFilesystem\\":false,\\"dockerLabels\\":null,\\"systemControls\\":null,\\"privileged\\":null,\\"name\\":\\"container2\\"},{\\"dnsSearchDomains\\":null,\\"environmentFiles\\":null,\\"logConfiguration\\":{\\"logDriver\\":\\"awslogs\\",\\"secretOptions\\":[],\\"options\\":{\\"awslogs-group\\":\\"\${aws_cloudwatch_log_group.testECSService_ecs-container3_1286AAA3.name}\\",\\"awslogs-region\\":\\"us-east-1\\",\\"awslogs-stream-prefix\\":\\"ecs\\"}},\\"entryPoint\\":null,\\"portMappings\\":[],\\"linuxParameters\\":null,\\"cpu\\":0,\\"environment\\":[],\\"resourceRequirements\\":null,\\"ulimits\\":null,\\"repositoryCredentials\\":null,\\"dnsServers\\":null,\\"mountPoints\\":[{\\"sourceVolume\\":\\"src1\\",\\"containerPath\\":\\"/src1\\",\\"readOnly\\":true},{\\"sourceVolume\\":\\"src3\\",\\"containerPath\\":\\"/src3\\"}],\\"workingDirectory\\":null,\\"secrets\\":null,\\"dockerSecurityOptions\\":null,\\"memory\\":null,\\"memoryReservation\\":null,\\"volumesFrom\\":[],\\"stopTimeout\\":null,\\"image\\":\\"\${aws_ecr_repository.testECSService_ecr-container3_ecr-repo_F25059FD.repository_url}:latest\\",\\"startTimeout\\":null,\\"firelensConfiguration\\":null,\\"dependsOn\\":null,\\"disableNetworking\\":null,\\"interactive\\":null,\\"healthCheck\\":null,\\"essential\\":true,\\"links\\":null,\\"hostname\\":null,\\"extraHosts\\":null,\\"pseudoTerminal\\":null,\\"user\\":null,\\"readonlyRootFilesystem\\":false,\\"dockerLabels\\":null,\\"systemControls\\":null,\\"privileged\\":null,\\"name\\":\\"container3\\"}]", + "cpu": "512", + "depends_on": [ + "aws_ecr_repository.testECSService_ecr-container1_ecr-repo_6BB90A2E", + "aws_ecr_repository.testECSService_ecr-container2_ecr-repo_6980E3F0", + "aws_ecr_repository.testECSService_ecr-container3_ecr-repo_F25059FD" + ], + "execution_role_arn": "\${aws_iam_role.testECSService_ecs-iam_ecs-execution-role_88EFEECE.arn}", + "family": "abides-dev", + "memory": "2048", + "network_mode": "awsvpc", + "requires_compatibilities": [ + "FARGATE" + ], + "task_role_arn": "\${aws_iam_role.testECSService_ecs-iam_ecs-task-role_83E10144.arn}", + "volume": [ + { + "name": "src1" + }, + { + "name": "src2" + }, + { + "name": "src3" + } + ] + } + }, + "aws_iam_role": { + "testECSService_ecs-iam_ecs-execution-role_88EFEECE": { + "assume_role_policy": "\${data.aws_iam_policy_document.testECSService_ecs-iam_ecs-task-assume_97906D9E.json}", + "name": "abides-dev-TaskExecutionRole" + }, + "testECSService_ecs-iam_ecs-task-role_83E10144": { + "assume_role_policy": "\${data.aws_iam_policy_document.testECSService_ecs-iam_ecs-task-assume_97906D9E.json}", + "name": "abides-dev-TaskRole" + } + }, + "aws_iam_role_policy_attachment": { + "testECSService_ecs-iam_ecs-task-execution-default-attachment_7AC36BF7": { + "policy_arn": "someArn", + "role": "\${aws_iam_role.testECSService_ecs-iam_ecs-execution-role_88EFEECE.id}" + } + }, + "aws_security_group": { + "testECSService_ecs_security_group_67979722": { + "description": "Internal ECS Security Group (Managed by Terraform)", + "egress": [ + { + "cidr_blocks": [ + "0.0.0.0/0" + ], + "description": "required", + "from_port": 0, + "ipv6_cidr_blocks": [ + ], + "prefix_list_ids": [ + ], + "protocol": "-1", + "security_groups": [ + ], + "self": null, + "to_port": 0 + } + ], + "ingress": [ + ], + "lifecycle": { + "create_before_destroy": true + }, + "name_prefix": "abides-dev-ECSSecurityGroup", + "vpc_id": "myhouse" + } + } + } +}" +`; + +exports[`ApplicationECSService renders an ECS service without a log group container definition props 1`] = ` +"{ + "data": { + "aws_iam_policy_document": { + "testECSService_ecs-iam_ecs-task-assume_97906D9E": { + "statement": [ + { + "actions": [ + "sts:AssumeRole" + ], + "effect": "Allow", + "principals": [ + { + "identifiers": [ + "ecs-tasks.amazonaws.com" + ], + "type": "Service" + } + ] + } + ], + "version": "2012-10-17" + } + } + }, + "resource": { + "aws_cloudwatch_log_group": { + "testECSService_ecs-lebowski_226D2007": { + "name_prefix": "/ecs/abides-dev/lebowski", + "retention_in_days": 30 + } + }, + "aws_ecs_service": { + "testECSService_ecs-service_33F309B3": { + "cluster": "gorp", + "depends_on": [ + ], + "deployment_controller": { + "type": "ECS" + }, + "deployment_maximum_percent": 400, + "deployment_minimum_healthy_percent": 80, + "desired_count": 4, + "launch_type": "ROCKET", + "lifecycle": { + "ignore_changes": [ + "bowling", + "donnie", + "autobahn" + ] + }, + "load_balancer": [ + ], + "name": "abides-dev", + "network_configuration": { + "security_groups": [ + "\${aws_security_group.testECSService_ecs_security_group_67979722.id}" + ], + "subnets": [ + "1.1.1.1", + "2.2.2.2" + ] + }, + "propagate_tags": "SERVICE", + "task_definition": "\${aws_ecs_task_definition.testECSService_ecs-task_A268C927.arn}" + } + }, + "aws_ecs_task_definition": { + "testECSService_ecs-task_A268C927": { + "container_definitions": "[{\\"dnsSearchDomains\\":null,\\"environmentFiles\\":null,\\"logConfiguration\\":{\\"logDriver\\":\\"awslogs\\",\\"secretOptions\\":[],\\"options\\":{\\"awslogs-group\\":\\"\${aws_cloudwatch_log_group.testECSService_ecs-lebowski_226D2007.name}\\",\\"awslogs-region\\":\\"us-east-1\\",\\"awslogs-stream-prefix\\":\\"ecs\\"}},\\"entryPoint\\":null,\\"portMappings\\":[{\\"containerPort\\":3002,\\"hostPort\\":3001}],\\"linuxParameters\\":null,\\"cpu\\":0,\\"environment\\":[{\\"name\\":\\"rug\\",\\"value\\":\\"tiedtheroomtogether\\"}],\\"resourceRequirements\\":null,\\"ulimits\\":null,\\"repositoryCredentials\\":{\\"credentialsParameter\\":\\"someArn\\"},\\"dnsServers\\":null,\\"mountPoints\\":[],\\"workingDirectory\\":null,\\"secrets\\":[{\\"name\\":\\"donnie\\",\\"valueFrom\\":\\"throwinrockstonight\\"}],\\"dockerSecurityOptions\\":null,\\"memory\\":null,\\"memoryReservation\\":null,\\"volumesFrom\\":[],\\"stopTimeout\\":null,\\"image\\":\\"beverage-here/0.1\\",\\"startTimeout\\":null,\\"firelensConfiguration\\":null,\\"dependsOn\\":null,\\"disableNetworking\\":null,\\"interactive\\":null,\\"healthCheck\\":null,\\"essential\\":true,\\"links\\":null,\\"hostname\\":null,\\"extraHosts\\":null,\\"pseudoTerminal\\":null,\\"user\\":null,\\"readonlyRootFilesystem\\":false,\\"dockerLabels\\":null,\\"systemControls\\":null,\\"privileged\\":null,\\"name\\":\\"lebowski\\"}]", + "cpu": "512", + "depends_on": [ + ], + "execution_role_arn": "\${aws_iam_role.testECSService_ecs-iam_ecs-execution-role_88EFEECE.arn}", + "family": "abides-dev", + "memory": "2048", + "network_mode": "awsvpc", + "requires_compatibilities": [ + "FARGATE" + ], + "task_role_arn": "\${aws_iam_role.testECSService_ecs-iam_ecs-task-role_83E10144.arn}", + "volume": [ + ] + } + }, + "aws_iam_role": { + "testECSService_ecs-iam_ecs-execution-role_88EFEECE": { + "assume_role_policy": "\${data.aws_iam_policy_document.testECSService_ecs-iam_ecs-task-assume_97906D9E.json}", + "name": "abides-dev-TaskExecutionRole" + }, + "testECSService_ecs-iam_ecs-task-role_83E10144": { + "assume_role_policy": "\${data.aws_iam_policy_document.testECSService_ecs-iam_ecs-task-assume_97906D9E.json}", + "name": "abides-dev-TaskRole" + } + }, + "aws_iam_role_policy_attachment": { + "testECSService_ecs-iam_ecs-task-execution-default-attachment_7AC36BF7": { + "policy_arn": "someArn", + "role": "\${aws_iam_role.testECSService_ecs-iam_ecs-execution-role_88EFEECE.id}" + } + }, + "aws_security_group": { + "testECSService_ecs_security_group_67979722": { + "description": "Internal ECS Security Group (Managed by Terraform)", + "egress": [ + { + "cidr_blocks": [ + "0.0.0.0/0" + ], + "description": "required", + "from_port": 0, + "ipv6_cidr_blocks": [ + ], + "prefix_list_ids": [ + ], + "protocol": "-1", + "security_groups": [ + ], + "self": null, + "to_port": 0 + } + ], + "ingress": [ + ], + "lifecycle": { + "create_before_destroy": true + }, + "name_prefix": "abides-dev-ECSSecurityGroup", + "vpc_id": "myhouse" + } + } + } +}" +`; + +exports[`ApplicationECSService renders an ECS service without an image container definition props 1`] = ` +"{ + "data": { + "aws_iam_policy_document": { + "testECSService_ecs-iam_ecs-task-assume_97906D9E": { + "statement": [ + { + "actions": [ + "sts:AssumeRole" + ], + "effect": "Allow", + "principals": [ + { + "identifiers": [ + "ecs-tasks.amazonaws.com" + ], + "type": "Service" + } + ] + } + ], + "version": "2012-10-17" + } + } + }, + "resource": { + "aws_ecr_lifecycle_policy": { + "testECSService_ecr-lebowski_ecr-repo-lifecyclepolicy_19C9732F": { + "depends_on": [ + "aws_ecr_repository.testECSService_ecr-lebowski_ecr-repo_6D07DF86" + ], + "policy": "{\\"rules\\":[{\\"rulePriority\\":1,\\"description\\":\\"expire old images\\",\\"selection\\":{\\"tagStatus\\":\\"any\\",\\"countType\\":\\"imageCountMoreThan\\",\\"countNumber\\":800},\\"action\\":{\\"type\\":\\"expire\\"}}]}", + "repository": "\${aws_ecr_repository.testECSService_ecr-lebowski_ecr-repo_6D07DF86.name}" + } + }, + "aws_ecr_repository": { + "testECSService_ecr-lebowski_ecr-repo_6D07DF86": { + "image_scanning_configuration": { + "scan_on_push": true + }, + "name": "abides-dev-lebowski" + } + }, + "aws_ecs_service": { + "testECSService_ecs-service_33F309B3": { + "cluster": "gorp", + "depends_on": [ + "aws_ecr_repository.testECSService_ecr-lebowski_ecr-repo_6D07DF86" + ], + "deployment_controller": { + "type": "ECS" + }, + "deployment_maximum_percent": 400, + "deployment_minimum_healthy_percent": 80, + "desired_count": 4, + "launch_type": "ROCKET", + "lifecycle": { + "ignore_changes": [ + "bowling", + "donnie", + "autobahn" + ] + }, + "load_balancer": [ + ], + "name": "abides-dev", + "network_configuration": { + "security_groups": [ + "\${aws_security_group.testECSService_ecs_security_group_67979722.id}" + ], + "subnets": [ + "1.1.1.1", + "2.2.2.2" + ] + }, + "propagate_tags": "SERVICE", + "task_definition": "\${aws_ecs_task_definition.testECSService_ecs-task_A268C927.arn}" + } + }, + "aws_ecs_task_definition": { + "testECSService_ecs-task_A268C927": { + "container_definitions": "[{\\"dnsSearchDomains\\":null,\\"environmentFiles\\":null,\\"logConfiguration\\":{\\"logDriver\\":\\"awslogs\\",\\"secretOptions\\":[],\\"options\\":{\\"awslogs-group\\":\\"test/log/group\\",\\"awslogs-region\\":\\"us-east-1\\",\\"awslogs-stream-prefix\\":\\"ecs\\"}},\\"entryPoint\\":null,\\"portMappings\\":[{\\"containerPort\\":3002,\\"hostPort\\":3001}],\\"linuxParameters\\":null,\\"cpu\\":0,\\"environment\\":[{\\"name\\":\\"rug\\",\\"value\\":\\"tiedtheroomtogether\\"}],\\"resourceRequirements\\":null,\\"ulimits\\":null,\\"repositoryCredentials\\":{\\"credentialsParameter\\":\\"someArn\\"},\\"dnsServers\\":null,\\"mountPoints\\":[],\\"workingDirectory\\":null,\\"secrets\\":[{\\"name\\":\\"donnie\\",\\"valueFrom\\":\\"throwinrockstonight\\"}],\\"dockerSecurityOptions\\":null,\\"memory\\":null,\\"memoryReservation\\":null,\\"volumesFrom\\":[],\\"stopTimeout\\":null,\\"image\\":\\"\${aws_ecr_repository.testECSService_ecr-lebowski_ecr-repo_6D07DF86.repository_url}:latest\\",\\"startTimeout\\":null,\\"firelensConfiguration\\":null,\\"dependsOn\\":null,\\"disableNetworking\\":null,\\"interactive\\":null,\\"healthCheck\\":null,\\"essential\\":true,\\"links\\":null,\\"hostname\\":null,\\"extraHosts\\":null,\\"pseudoTerminal\\":null,\\"user\\":null,\\"readonlyRootFilesystem\\":false,\\"dockerLabels\\":null,\\"systemControls\\":null,\\"privileged\\":null,\\"name\\":\\"lebowski\\"}]", + "cpu": "512", + "depends_on": [ + "aws_ecr_repository.testECSService_ecr-lebowski_ecr-repo_6D07DF86" + ], + "execution_role_arn": "\${aws_iam_role.testECSService_ecs-iam_ecs-execution-role_88EFEECE.arn}", + "family": "abides-dev", + "memory": "2048", + "network_mode": "awsvpc", + "requires_compatibilities": [ + "FARGATE" + ], + "task_role_arn": "\${aws_iam_role.testECSService_ecs-iam_ecs-task-role_83E10144.arn}", + "volume": [ + ] + } + }, + "aws_iam_role": { + "testECSService_ecs-iam_ecs-execution-role_88EFEECE": { + "assume_role_policy": "\${data.aws_iam_policy_document.testECSService_ecs-iam_ecs-task-assume_97906D9E.json}", + "name": "abides-dev-TaskExecutionRole" + }, + "testECSService_ecs-iam_ecs-task-role_83E10144": { + "assume_role_policy": "\${data.aws_iam_policy_document.testECSService_ecs-iam_ecs-task-assume_97906D9E.json}", + "name": "abides-dev-TaskRole" + } + }, + "aws_iam_role_policy_attachment": { + "testECSService_ecs-iam_ecs-task-execution-default-attachment_7AC36BF7": { + "policy_arn": "someArn", + "role": "\${aws_iam_role.testECSService_ecs-iam_ecs-execution-role_88EFEECE.id}" + } + }, + "aws_security_group": { + "testECSService_ecs_security_group_67979722": { + "description": "Internal ECS Security Group (Managed by Terraform)", + "egress": [ + { + "cidr_blocks": [ + "0.0.0.0/0" + ], + "description": "required", + "from_port": 0, + "ipv6_cidr_blocks": [ + ], + "prefix_list_ids": [ + ], + "protocol": "-1", + "security_groups": [ + ], + "self": null, + "to_port": 0 + } + ], + "ingress": [ + ], + "lifecycle": { + "create_before_destroy": true + }, + "name_prefix": "abides-dev-ECSSecurityGroup", + "vpc_id": "myhouse" + } + } + } +}" +`; diff --git a/packages/terraform-modules/src/base/__snapshots__/ApplicationEventBridgeRule.spec.ts.snap b/packages/terraform-modules/src/base/__snapshots__/ApplicationEventBridgeRule.spec.ts.snap new file mode 100644 index 00000000..307f18c2 --- /dev/null +++ b/packages/terraform-modules/src/base/__snapshots__/ApplicationEventBridgeRule.spec.ts.snap @@ -0,0 +1,152 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`AplicationEventBridgeRule renders an event bridge with description 1`] = ` +"{ + "resource": { + "aws_cloudwatch_event_rule": { + "test-event-bridge-rule_DA4F2E87": { + "description": "Test description", + "event_bus_name": "default", + "event_pattern": "{\\"source\\":[\\"aws.states\\"],\\"detail-type\\":[\\"Step Functions Execution Status Change\\"]}", + "lifecycle": { + }, + "name": "Test-EventBridge-Rule" + } + } + } +}" +`; + +exports[`AplicationEventBridgeRule renders an event bridge with event bus name 1`] = ` +"{ + "resource": { + "aws_cloudwatch_event_rule": { + "test-event-bridge-rule_DA4F2E87": { + "event_bus_name": "test-bus", + "event_pattern": "{\\"source\\":[\\"aws.states\\"],\\"detail-type\\":[\\"Step Functions Execution Status Change\\"]}", + "lifecycle": { + }, + "name": "Test-EventBridge-Rule" + } + } + } +}" +`; + +exports[`AplicationEventBridgeRule renders an event bridge with pre-existing sqs target 1`] = ` +"{ + "resource": { + "aws_cloudwatch_event_rule": { + "test-event-bridge-rule_DA4F2E87": { + "event_bus_name": "default", + "event_pattern": "{\\"source\\":[\\"aws.states\\"],\\"detail-type\\":[\\"Step Functions Execution Status Change\\"]}", + "lifecycle": { + }, + "name": "Test-EventBridge-Rule" + } + }, + "aws_cloudwatch_event_target": { + "test-event-bridge-rule_event-bridge-target-sqs_E09C359A": { + "arn": "\${aws_sqs_queue.test-queue.arn}", + "dead_letter_config": { + }, + "event_bus_name": "default", + "rule": "\${aws_cloudwatch_event_rule.test-event-bridge-rule_DA4F2E87.name}", + "target_id": "sqs" + } + }, + "aws_sqs_queue": { + "test-queue": { + "name": "Test-SQS-Queue" + } + } + } +}" +`; + +exports[`AplicationEventBridgeRule renders an event bridge with scheduleExpression 1`] = ` +"{ + "resource": { + "aws_cloudwatch_event_rule": { + "test-event-bridge-rule_DA4F2E87": { + "event_bus_name": "default", + "lifecycle": { + }, + "name": "Test-EventBridge-Rule", + "schedule_expression": "rate(1 minute)" + } + } + } +}" +`; + +exports[`AplicationEventBridgeRule renders an event bridge with sqs target 1`] = ` +"{ + "resource": { + "aws_cloudwatch_event_rule": { + "test-event-bridge-rule_DA4F2E87": { + "event_bus_name": "default", + "event_pattern": "{\\"source\\":[\\"aws.states\\"],\\"detail-type\\":[\\"Step Functions Execution Status Change\\"]}", + "lifecycle": { + }, + "name": "Test-EventBridge-Rule" + } + }, + "aws_cloudwatch_event_target": { + "test-event-bridge-rule_event-bridge-target-sqs_E09C359A": { + "arn": "\${aws_sqs_queue.test-queue.arn}", + "dead_letter_config": { + }, + "depends_on": [ + "aws_sqs_queue.test-queue", + "aws_cloudwatch_event_rule.test-event-bridge-rule_DA4F2E87" + ], + "event_bus_name": "default", + "rule": "\${aws_cloudwatch_event_rule.test-event-bridge-rule_DA4F2E87.name}", + "target_id": "sqs" + } + }, + "aws_sqs_queue": { + "test-queue": { + "name": "Test-SQS-Queue" + } + } + } +}" +`; + +exports[`AplicationEventBridgeRule renders an event bridge with tags 1`] = ` +"{ + "resource": { + "aws_cloudwatch_event_rule": { + "test-event-bridge-rule_DA4F2E87": { + "event_bus_name": "default", + "event_pattern": "{\\"source\\":[\\"aws.states\\"],\\"detail-type\\":[\\"Step Functions Execution Status Change\\"]}", + "lifecycle": { + }, + "name": "Test-EventBridge-Rule", + "tags": { + "for": "test", + "my": "tag" + } + } + } + } +}" +`; + +exports[`AplicationEventBridgeRule renders an event bridge without a target 1`] = ` +"{ + "resource": { + "aws_cloudwatch_event_rule": { + "test-event-bridge-rule_DA4F2E87": { + "event_bus_name": "default", + "event_pattern": "{\\"source\\":[\\"aws.states\\"],\\"detail-type\\":[\\"Step Functions Execution Status Change\\"]}", + "lifecycle": { + }, + "name": "Test-EventBridge-Rule" + } + } + } +}" +`; diff --git a/packages/terraform-modules/src/base/__snapshots__/ApplicationEventBus.spec.ts.snap b/packages/terraform-modules/src/base/__snapshots__/ApplicationEventBus.spec.ts.snap new file mode 100644 index 00000000..e148d8bf --- /dev/null +++ b/packages/terraform-modules/src/base/__snapshots__/ApplicationEventBus.spec.ts.snap @@ -0,0 +1,16 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ApplicationEventBus renders an event bus with name and target 1`] = ` +"{ + "resource": { + "aws_cloudwatch_event_bus": { + "test-event-bus_event-bus-test-event-bus_3B38546A": { + "name": "test-event-bus", + "tags": { + "service": "test-service" + } + } + } + } +}" +`; diff --git a/packages/terraform-modules/src/base/__snapshots__/ApplicationLambdaCodeDeploy.spec.ts.snap b/packages/terraform-modules/src/base/__snapshots__/ApplicationLambdaCodeDeploy.spec.ts.snap new file mode 100644 index 00000000..ee2dd7d0 --- /dev/null +++ b/packages/terraform-modules/src/base/__snapshots__/ApplicationLambdaCodeDeploy.spec.ts.snap @@ -0,0 +1,505 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ApplicationLambdaCodeDeploy renders a lambda code deploy app 1`] = ` +"{ + "data": { + "aws_iam_policy_document": { + "test-lambda-code-deploy_code-deploy-assume-role-policy-document_484EBB89": { + "statement": [ + { + "actions": [ + "sts:AssumeRole" + ], + "effect": "Allow", + "principals": [ + { + "identifiers": [ + "codedeploy.amazonaws.com" + ], + "type": "Service" + } + ] + } + ] + } + } + }, + "resource": { + "aws_codedeploy_app": { + "test-lambda-code-deploy_code-deploy-app_A7640CFE": { + "compute_platform": "Lambda", + "name": "Test-Lambda-Code-Deploy-Lambda" + } + }, + "aws_codedeploy_deployment_group": { + "test-lambda-code-deploy_code-deployment-group_3F73EBCE": { + "app_name": "\${aws_codedeploy_app.test-lambda-code-deploy_code-deploy-app_A7640CFE.name}", + "auto_rollback_configuration": { + "enabled": true, + "events": [ + "DEPLOYMENT_FAILURE" + ] + }, + "depends_on": [ + "aws_codedeploy_app.test-lambda-code-deploy_code-deploy-app_A7640CFE" + ], + "deployment_config_name": "CodeDeployDefault.LambdaAllAtOnce", + "deployment_group_name": "\${aws_codedeploy_app.test-lambda-code-deploy_code-deploy-app_A7640CFE.name}", + "deployment_style": { + "deployment_option": "WITH_TRAFFIC_CONTROL", + "deployment_type": "BLUE_GREEN" + }, + "service_role_arn": "\${aws_iam_role.test-lambda-code-deploy_code-deploy-role_9DA8B07C.arn}" + } + }, + "aws_iam_role": { + "test-lambda-code-deploy_code-deploy-role_9DA8B07C": { + "assume_role_policy": "\${data.aws_iam_policy_document.test-lambda-code-deploy_code-deploy-assume-role-policy-document_484EBB89.json}", + "name": "Test-Lambda-Code-Deploy-CodeDeployRole" + } + }, + "aws_iam_role_policy_attachment": { + "test-lambda-code-deploy_code-deploy-policy-attachment_84E423E0": { + "depends_on": [ + "aws_iam_role.test-lambda-code-deploy_code-deploy-role_9DA8B07C" + ], + "policy_arn": "arn:aws:iam::aws:policy/service-role/AWSCodeDeployRoleForLambda", + "role": "\${aws_iam_role.test-lambda-code-deploy_code-deploy-role_9DA8B07C.name}" + } + } + } +}" +`; + +exports[`ApplicationLambdaCodeDeploy renders a lambda code deploy app with all notifications enabled 1`] = ` +"{ + "data": { + "aws_iam_policy_document": { + "test-lambda-code-deploy_code-deploy-assume-role-policy-document_484EBB89": { + "statement": [ + { + "actions": [ + "sts:AssumeRole" + ], + "effect": "Allow", + "principals": [ + { + "identifiers": [ + "codedeploy.amazonaws.com" + ], + "type": "Service" + } + ] + } + ] + } + } + }, + "resource": { + "aws_codedeploy_app": { + "test-lambda-code-deploy_code-deploy-app_A7640CFE": { + "compute_platform": "Lambda", + "name": "Test-Lambda-Code-Deploy-Lambda" + } + }, + "aws_codedeploy_deployment_group": { + "test-lambda-code-deploy_code-deployment-group_3F73EBCE": { + "app_name": "\${aws_codedeploy_app.test-lambda-code-deploy_code-deploy-app_A7640CFE.name}", + "auto_rollback_configuration": { + "enabled": true, + "events": [ + "DEPLOYMENT_FAILURE" + ] + }, + "depends_on": [ + "aws_codedeploy_app.test-lambda-code-deploy_code-deploy-app_A7640CFE" + ], + "deployment_config_name": "CodeDeployDefault.LambdaAllAtOnce", + "deployment_group_name": "\${aws_codedeploy_app.test-lambda-code-deploy_code-deploy-app_A7640CFE.name}", + "deployment_style": { + "deployment_option": "WITH_TRAFFIC_CONTROL", + "deployment_type": "BLUE_GREEN" + }, + "service_role_arn": "\${aws_iam_role.test-lambda-code-deploy_code-deploy-role_9DA8B07C.arn}" + } + }, + "aws_codestarnotifications_notification_rule": { + "test-lambda-code-deploy_notifications_7F614EE2": { + "detail_type": "BASIC", + "event_type_ids": [ + "codedeploy-application-deployment-failed", + "codedeploy-application-deployment-succeeded", + "codedeploy-application-deployment-started" + ], + "name": "\${aws_codedeploy_app.test-lambda-code-deploy_code-deploy-app_A7640CFE.name}", + "resource": "arn:aws:codedeploy:us-east-1:123:application:\${aws_codedeploy_app.test-lambda-code-deploy_code-deploy-app_A7640CFE.name}", + "target": [ + { + "address": "test:deploy-topic:arn" + } + ] + } + }, + "aws_iam_role": { + "test-lambda-code-deploy_code-deploy-role_9DA8B07C": { + "assume_role_policy": "\${data.aws_iam_policy_document.test-lambda-code-deploy_code-deploy-assume-role-policy-document_484EBB89.json}", + "name": "Test-Lambda-Code-Deploy-CodeDeployRole" + } + }, + "aws_iam_role_policy_attachment": { + "test-lambda-code-deploy_code-deploy-policy-attachment_84E423E0": { + "depends_on": [ + "aws_iam_role.test-lambda-code-deploy_code-deploy-role_9DA8B07C" + ], + "policy_arn": "arn:aws:iam::aws:policy/service-role/AWSCodeDeployRoleForLambda", + "role": "\${aws_iam_role.test-lambda-code-deploy_code-deploy-role_9DA8B07C.name}" + } + } + } +}" +`; + +exports[`ApplicationLambdaCodeDeploy renders a lambda code deploy app with default notifications 1`] = ` +"{ + "data": { + "aws_iam_policy_document": { + "test-lambda-code-deploy_code-deploy-assume-role-policy-document_484EBB89": { + "statement": [ + { + "actions": [ + "sts:AssumeRole" + ], + "effect": "Allow", + "principals": [ + { + "identifiers": [ + "codedeploy.amazonaws.com" + ], + "type": "Service" + } + ] + } + ] + } + } + }, + "resource": { + "aws_codedeploy_app": { + "test-lambda-code-deploy_code-deploy-app_A7640CFE": { + "compute_platform": "Lambda", + "name": "Test-Lambda-Code-Deploy-Lambda" + } + }, + "aws_codedeploy_deployment_group": { + "test-lambda-code-deploy_code-deployment-group_3F73EBCE": { + "app_name": "\${aws_codedeploy_app.test-lambda-code-deploy_code-deploy-app_A7640CFE.name}", + "auto_rollback_configuration": { + "enabled": true, + "events": [ + "DEPLOYMENT_FAILURE" + ] + }, + "depends_on": [ + "aws_codedeploy_app.test-lambda-code-deploy_code-deploy-app_A7640CFE" + ], + "deployment_config_name": "CodeDeployDefault.LambdaAllAtOnce", + "deployment_group_name": "\${aws_codedeploy_app.test-lambda-code-deploy_code-deploy-app_A7640CFE.name}", + "deployment_style": { + "deployment_option": "WITH_TRAFFIC_CONTROL", + "deployment_type": "BLUE_GREEN" + }, + "service_role_arn": "\${aws_iam_role.test-lambda-code-deploy_code-deploy-role_9DA8B07C.arn}" + } + }, + "aws_codestarnotifications_notification_rule": { + "test-lambda-code-deploy_notifications_7F614EE2": { + "detail_type": "BASIC", + "event_type_ids": [ + "codedeploy-application-deployment-failed" + ], + "name": "\${aws_codedeploy_app.test-lambda-code-deploy_code-deploy-app_A7640CFE.name}", + "resource": "arn:aws:codedeploy:us-east-1:123:application:\${aws_codedeploy_app.test-lambda-code-deploy_code-deploy-app_A7640CFE.name}", + "target": [ + { + "address": "test:deploy-topic:arn" + } + ] + } + }, + "aws_iam_role": { + "test-lambda-code-deploy_code-deploy-role_9DA8B07C": { + "assume_role_policy": "\${data.aws_iam_policy_document.test-lambda-code-deploy_code-deploy-assume-role-policy-document_484EBB89.json}", + "name": "Test-Lambda-Code-Deploy-CodeDeployRole" + } + }, + "aws_iam_role_policy_attachment": { + "test-lambda-code-deploy_code-deploy-policy-attachment_84E423E0": { + "depends_on": [ + "aws_iam_role.test-lambda-code-deploy_code-deploy-role_9DA8B07C" + ], + "policy_arn": "arn:aws:iam::aws:policy/service-role/AWSCodeDeployRoleForLambda", + "role": "\${aws_iam_role.test-lambda-code-deploy_code-deploy-role_9DA8B07C.name}" + } + } + } +}" +`; + +exports[`ApplicationLambdaCodeDeploy renders a lambda code deploy app with sns topic arn 1`] = ` +"{ + "data": { + "aws_iam_policy_document": { + "test-lambda-code-deploy_code-deploy-assume-role-policy-document_484EBB89": { + "statement": [ + { + "actions": [ + "sts:AssumeRole" + ], + "effect": "Allow", + "principals": [ + { + "identifiers": [ + "codedeploy.amazonaws.com" + ], + "type": "Service" + } + ] + } + ] + } + } + }, + "resource": { + "aws_codedeploy_app": { + "test-lambda-code-deploy_code-deploy-app_A7640CFE": { + "compute_platform": "Lambda", + "name": "Test-Lambda-Code-Deploy-Lambda" + } + }, + "aws_codedeploy_deployment_group": { + "test-lambda-code-deploy_code-deployment-group_3F73EBCE": { + "app_name": "\${aws_codedeploy_app.test-lambda-code-deploy_code-deploy-app_A7640CFE.name}", + "auto_rollback_configuration": { + "enabled": true, + "events": [ + "DEPLOYMENT_FAILURE" + ] + }, + "depends_on": [ + "aws_codedeploy_app.test-lambda-code-deploy_code-deploy-app_A7640CFE" + ], + "deployment_config_name": "CodeDeployDefault.LambdaAllAtOnce", + "deployment_group_name": "\${aws_codedeploy_app.test-lambda-code-deploy_code-deploy-app_A7640CFE.name}", + "deployment_style": { + "deployment_option": "WITH_TRAFFIC_CONTROL", + "deployment_type": "BLUE_GREEN" + }, + "service_role_arn": "\${aws_iam_role.test-lambda-code-deploy_code-deploy-role_9DA8B07C.arn}" + } + }, + "aws_codestarnotifications_notification_rule": { + "test-lambda-code-deploy_notifications_7F614EE2": { + "detail_type": "BASIC", + "event_type_ids": [ + "codedeploy-application-deployment-failed" + ], + "name": "\${aws_codedeploy_app.test-lambda-code-deploy_code-deploy-app_A7640CFE.name}", + "resource": "arn:aws:codedeploy:us-east-1:123:application:\${aws_codedeploy_app.test-lambda-code-deploy_code-deploy-app_A7640CFE.name}", + "target": [ + { + "address": "test:deploy-topic:arn" + } + ] + } + }, + "aws_iam_role": { + "test-lambda-code-deploy_code-deploy-role_9DA8B07C": { + "assume_role_policy": "\${data.aws_iam_policy_document.test-lambda-code-deploy_code-deploy-assume-role-policy-document_484EBB89.json}", + "name": "Test-Lambda-Code-Deploy-CodeDeployRole" + } + }, + "aws_iam_role_policy_attachment": { + "test-lambda-code-deploy_code-deploy-policy-attachment_84E423E0": { + "depends_on": [ + "aws_iam_role.test-lambda-code-deploy_code-deploy-role_9DA8B07C" + ], + "policy_arn": "arn:aws:iam::aws:policy/service-role/AWSCodeDeployRoleForLambda", + "role": "\${aws_iam_role.test-lambda-code-deploy_code-deploy-role_9DA8B07C.name}" + } + } + } +}" +`; + +exports[`ApplicationLambdaCodeDeploy renders a lambda code deploy app with sns topic arn and detail type 1`] = ` +"{ + "data": { + "aws_iam_policy_document": { + "test-lambda-code-deploy_code-deploy-assume-role-policy-document_484EBB89": { + "statement": [ + { + "actions": [ + "sts:AssumeRole" + ], + "effect": "Allow", + "principals": [ + { + "identifiers": [ + "codedeploy.amazonaws.com" + ], + "type": "Service" + } + ] + } + ] + } + } + }, + "resource": { + "aws_codedeploy_app": { + "test-lambda-code-deploy_code-deploy-app_A7640CFE": { + "compute_platform": "Lambda", + "name": "Test-Lambda-Code-Deploy-Lambda" + } + }, + "aws_codedeploy_deployment_group": { + "test-lambda-code-deploy_code-deployment-group_3F73EBCE": { + "app_name": "\${aws_codedeploy_app.test-lambda-code-deploy_code-deploy-app_A7640CFE.name}", + "auto_rollback_configuration": { + "enabled": true, + "events": [ + "DEPLOYMENT_FAILURE" + ] + }, + "depends_on": [ + "aws_codedeploy_app.test-lambda-code-deploy_code-deploy-app_A7640CFE" + ], + "deployment_config_name": "CodeDeployDefault.LambdaAllAtOnce", + "deployment_group_name": "\${aws_codedeploy_app.test-lambda-code-deploy_code-deploy-app_A7640CFE.name}", + "deployment_style": { + "deployment_option": "WITH_TRAFFIC_CONTROL", + "deployment_type": "BLUE_GREEN" + }, + "service_role_arn": "\${aws_iam_role.test-lambda-code-deploy_code-deploy-role_9DA8B07C.arn}" + } + }, + "aws_codestarnotifications_notification_rule": { + "test-lambda-code-deploy_notifications_7F614EE2": { + "detail_type": "FULL", + "event_type_ids": [ + "codedeploy-application-deployment-failed" + ], + "name": "\${aws_codedeploy_app.test-lambda-code-deploy_code-deploy-app_A7640CFE.name}", + "resource": "arn:aws:codedeploy:us-east-1:123:application:\${aws_codedeploy_app.test-lambda-code-deploy_code-deploy-app_A7640CFE.name}", + "target": [ + { + "address": "test:deploy-topic:arn" + } + ] + } + }, + "aws_iam_role": { + "test-lambda-code-deploy_code-deploy-role_9DA8B07C": { + "assume_role_policy": "\${data.aws_iam_policy_document.test-lambda-code-deploy_code-deploy-assume-role-policy-document_484EBB89.json}", + "name": "Test-Lambda-Code-Deploy-CodeDeployRole" + } + }, + "aws_iam_role_policy_attachment": { + "test-lambda-code-deploy_code-deploy-policy-attachment_84E423E0": { + "depends_on": [ + "aws_iam_role.test-lambda-code-deploy_code-deploy-role_9DA8B07C" + ], + "policy_arn": "arn:aws:iam::aws:policy/service-role/AWSCodeDeployRoleForLambda", + "role": "\${aws_iam_role.test-lambda-code-deploy_code-deploy-role_9DA8B07C.name}" + } + } + } +}" +`; + +exports[`ApplicationLambdaCodeDeploy renders a lambda code deploy app with started and succeeded notifications only 1`] = ` +"{ + "data": { + "aws_iam_policy_document": { + "test-lambda-code-deploy_code-deploy-assume-role-policy-document_484EBB89": { + "statement": [ + { + "actions": [ + "sts:AssumeRole" + ], + "effect": "Allow", + "principals": [ + { + "identifiers": [ + "codedeploy.amazonaws.com" + ], + "type": "Service" + } + ] + } + ] + } + } + }, + "resource": { + "aws_codedeploy_app": { + "test-lambda-code-deploy_code-deploy-app_A7640CFE": { + "compute_platform": "Lambda", + "name": "Test-Lambda-Code-Deploy-Lambda" + } + }, + "aws_codedeploy_deployment_group": { + "test-lambda-code-deploy_code-deployment-group_3F73EBCE": { + "app_name": "\${aws_codedeploy_app.test-lambda-code-deploy_code-deploy-app_A7640CFE.name}", + "auto_rollback_configuration": { + "enabled": true, + "events": [ + "DEPLOYMENT_FAILURE" + ] + }, + "depends_on": [ + "aws_codedeploy_app.test-lambda-code-deploy_code-deploy-app_A7640CFE" + ], + "deployment_config_name": "CodeDeployDefault.LambdaAllAtOnce", + "deployment_group_name": "\${aws_codedeploy_app.test-lambda-code-deploy_code-deploy-app_A7640CFE.name}", + "deployment_style": { + "deployment_option": "WITH_TRAFFIC_CONTROL", + "deployment_type": "BLUE_GREEN" + }, + "service_role_arn": "\${aws_iam_role.test-lambda-code-deploy_code-deploy-role_9DA8B07C.arn}" + } + }, + "aws_codestarnotifications_notification_rule": { + "test-lambda-code-deploy_notifications_7F614EE2": { + "detail_type": "BASIC", + "event_type_ids": [ + "codedeploy-application-deployment-succeeded", + "codedeploy-application-deployment-started" + ], + "name": "\${aws_codedeploy_app.test-lambda-code-deploy_code-deploy-app_A7640CFE.name}", + "resource": "arn:aws:codedeploy:us-east-1:123:application:\${aws_codedeploy_app.test-lambda-code-deploy_code-deploy-app_A7640CFE.name}", + "target": [ + { + "address": "test:deploy-topic:arn" + } + ] + } + }, + "aws_iam_role": { + "test-lambda-code-deploy_code-deploy-role_9DA8B07C": { + "assume_role_policy": "\${data.aws_iam_policy_document.test-lambda-code-deploy_code-deploy-assume-role-policy-document_484EBB89.json}", + "name": "Test-Lambda-Code-Deploy-CodeDeployRole" + } + }, + "aws_iam_role_policy_attachment": { + "test-lambda-code-deploy_code-deploy-policy-attachment_84E423E0": { + "depends_on": [ + "aws_iam_role.test-lambda-code-deploy_code-deploy-role_9DA8B07C" + ], + "policy_arn": "arn:aws:iam::aws:policy/service-role/AWSCodeDeployRoleForLambda", + "role": "\${aws_iam_role.test-lambda-code-deploy_code-deploy-role_9DA8B07C.name}" + } + } + } +}" +`; diff --git a/packages/terraform-modules/src/base/__snapshots__/ApplicationLambdaSnsTopicSubscription.spec.ts.snap b/packages/terraform-modules/src/base/__snapshots__/ApplicationLambdaSnsTopicSubscription.spec.ts.snap new file mode 100644 index 00000000..1e57275a --- /dev/null +++ b/packages/terraform-modules/src/base/__snapshots__/ApplicationLambdaSnsTopicSubscription.spec.ts.snap @@ -0,0 +1,162 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ApplicationSqsSnsTopicSubscription renders an Lambda <> SNS subscription without tags 1`] = ` +"{ + "data": { + "aws_iam_policy_document": { + "lambda-sns-subscription_sns-dlq-policy-document_8DAB362F": { + "depends_on": [ + "aws_sqs_queue.lambda-sns-subscription_sns-topic-dlq_C5D5F199" + ], + "statement": [ + { + "actions": [ + "sqs:SendMessage" + ], + "condition": [ + { + "test": "ArnEquals", + "values": [ + "arn:aws:sns:TopicName" + ], + "variable": "aws:SourceArn" + } + ], + "effect": "Allow", + "principals": [ + { + "identifiers": [ + "sns.amazonaws.com" + ], + "type": "Service" + } + ], + "resources": [ + "\${aws_sqs_queue.lambda-sns-subscription_sns-topic-dlq_C5D5F199.arn}" + ] + } + ] + } + }, + "aws_lambda_function": { + "lambda": { + "function_name": "test-lambda" + } + } + }, + "resource": { + "aws_lambda_permission": { + "lambda-sns-subscription_lambda-sns-subscription-lambda-permission_03B5A953": { + "action": "lambda:InvokeFunction", + "function_name": "\${data.aws_lambda_function.lambda.function_name}", + "principal": "sns.amazonaws.com", + "source_arn": "arn:aws:sns:TopicName" + } + }, + "aws_sns_topic_subscription": { + "lambda-sns-subscription_1ED18AE9": { + "depends_on": [ + "aws_sqs_queue.lambda-sns-subscription_sns-topic-dlq_C5D5F199" + ], + "endpoint": "\${data.aws_lambda_function.lambda.arn}", + "protocol": "lambda", + "redrive_policy": "{\\"deadLetterTargetArn\\":\\"\${aws_sqs_queue.lambda-sns-subscription_sns-topic-dlq_C5D5F199.arn}\\"}", + "topic_arn": "arn:aws:sns:TopicName" + } + }, + "aws_sqs_queue": { + "lambda-sns-subscription_sns-topic-dlq_C5D5F199": { + "name": "test-sns-subscription-SNS-Topic-DLQ" + } + }, + "aws_sqs_queue_policy": { + "lambda-sns-subscription_sns-dlq-policy_31243636": { + "policy": "\${data.aws_iam_policy_document.lambda-sns-subscription_sns-dlq-policy-document_8DAB362F.json}", + "queue_url": "\${aws_sqs_queue.lambda-sns-subscription_sns-topic-dlq_C5D5F199.url}" + } + } + } +}" +`; + +exports[`ApplicationSqsSnsTopicSubscription renders an SQS SNS subscription with tags 1`] = ` +"{ + "data": { + "aws_iam_policy_document": { + "lambda-sns-subscription_sns-dlq-policy-document_8DAB362F": { + "depends_on": [ + "aws_sqs_queue.lambda-sns-subscription_sns-topic-dlq_C5D5F199" + ], + "statement": [ + { + "actions": [ + "sqs:SendMessage" + ], + "condition": [ + { + "test": "ArnEquals", + "values": [ + "arn:aws:sns:TopicName" + ], + "variable": "aws:SourceArn" + } + ], + "effect": "Allow", + "principals": [ + { + "identifiers": [ + "sns.amazonaws.com" + ], + "type": "Service" + } + ], + "resources": [ + "\${aws_sqs_queue.lambda-sns-subscription_sns-topic-dlq_C5D5F199.arn}" + ] + } + ] + } + }, + "aws_lambda_function": { + "lambda": { + "function_name": "test-lambda" + } + } + }, + "resource": { + "aws_lambda_permission": { + "lambda-sns-subscription_lambda-sns-subscription-lambda-permission_03B5A953": { + "action": "lambda:InvokeFunction", + "function_name": "\${data.aws_lambda_function.lambda.function_name}", + "principal": "sns.amazonaws.com", + "source_arn": "arn:aws:sns:TopicName" + } + }, + "aws_sns_topic_subscription": { + "lambda-sns-subscription_1ED18AE9": { + "depends_on": [ + "aws_sqs_queue.lambda-sns-subscription_sns-topic-dlq_C5D5F199" + ], + "endpoint": "\${data.aws_lambda_function.lambda.arn}", + "protocol": "lambda", + "redrive_policy": "{\\"deadLetterTargetArn\\":\\"\${aws_sqs_queue.lambda-sns-subscription_sns-topic-dlq_C5D5F199.arn}\\"}", + "topic_arn": "arn:aws:sns:TopicName" + } + }, + "aws_sqs_queue": { + "lambda-sns-subscription_sns-topic-dlq_C5D5F199": { + "name": "test-sns-subscription-SNS-Topic-DLQ", + "tags": { + "hello": "there" + } + } + }, + "aws_sqs_queue_policy": { + "lambda-sns-subscription_sns-dlq-policy_31243636": { + "policy": "\${data.aws_iam_policy_document.lambda-sns-subscription_sns-dlq-policy-document_8DAB362F.json}", + "queue_url": "\${aws_sqs_queue.lambda-sns-subscription_sns-topic-dlq_C5D5F199.url}" + } + } + } +}" +`; diff --git a/packages/terraform-modules/src/base/__snapshots__/ApplicationLoadBalancer.spec.ts.snap b/packages/terraform-modules/src/base/__snapshots__/ApplicationLoadBalancer.spec.ts.snap new file mode 100644 index 00000000..6186e4a7 --- /dev/null +++ b/packages/terraform-modules/src/base/__snapshots__/ApplicationLoadBalancer.spec.ts.snap @@ -0,0 +1,627 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ApplicationLoadBalancer renders an ALB with logs with a new bucket 1`] = ` +"{ + "data": { + "aws_elb_service_account": { + "testALB_elb-service-account_4BDD06CE": { + } + }, + "aws_iam_policy_document": { + "testALB_iam-log-bucket-policy-document_C7E41154": { + "statement": [ + { + "actions": [ + "s3:PutObject" + ], + "effect": "Allow", + "principals": [ + { + "identifiers": [ + "arn:aws:iam::\${data.aws_elb_service_account.testALB_elb-service-account_4BDD06CE.id}:root" + ], + "type": "AWS" + } + ], + "resources": [ + "arn:aws:s3:::\${aws_s3_bucket.testALB_log-bucket_E9787BB1.bucket}/*" + ] + } + ] + } + } + }, + "resource": { + "aws_alb": { + "testALB_alb_F6B33218": { + "access_logs": { + "bucket": "\${aws_s3_bucket.testALB_log-bucket_E9787BB1.bucket}", + "enabled": true, + "prefix": "server-logs/test-/alb" + }, + "internal": false, + "name_prefix": "TEST", + "security_groups": [ + "\${aws_security_group.testALB_alb_security_group_57C45F23.id}" + ], + "subnets": [ + "a", + "b" + ], + "tags": { + "hobby": "bowling", + "name": "thedude" + } + } + }, + "aws_s3_bucket": { + "testALB_log-bucket_E9787BB1": { + "bucket": "logging-bucket", + "tags": { + "hobby": "bowling", + "name": "thedude" + } + } + }, + "aws_s3_bucket_policy": { + "testALB_log-bucket-policy_EB762FB5": { + "bucket": "\${aws_s3_bucket.testALB_log-bucket_E9787BB1.bucket}", + "policy": "\${data.aws_iam_policy_document.testALB_iam-log-bucket-policy-document_C7E41154.json}" + } + }, + "aws_security_group": { + "testALB_alb_security_group_57C45F23": { + "description": "External security group (Managed by Terraform)", + "egress": [ + { + "cidr_blocks": [ + "0.0.0.0/0" + ], + "description": "required", + "from_port": 0, + "ipv6_cidr_blocks": [ + ], + "prefix_list_ids": [ + ], + "protocol": "-1", + "security_groups": [ + ], + "self": null, + "to_port": 0 + } + ], + "ingress": [ + { + "cidr_blocks": [ + "0.0.0.0/0" + ], + "description": null, + "from_port": 443, + "ipv6_cidr_blocks": null, + "prefix_list_ids": null, + "protocol": "TCP", + "security_groups": null, + "self": null, + "to_port": 443 + }, + { + "cidr_blocks": [ + "0.0.0.0/0" + ], + "description": null, + "from_port": 80, + "ipv6_cidr_blocks": null, + "prefix_list_ids": null, + "protocol": "TCP", + "security_groups": null, + "self": null, + "to_port": 80 + } + ], + "lifecycle": { + "create_before_destroy": true + }, + "name_prefix": "test--HTTP/S Security Group", + "tags": { + "Name": "test--HTTP/S Security Group", + "hobby": "bowling", + "name": "thedude" + }, + "vpc_id": "123" + } + } + } +}" +`; + +exports[`ApplicationLoadBalancer renders an ALB with logs with a new bucket and prefix 1`] = ` +"{ + "data": { + "aws_elb_service_account": { + "testALB_elb-service-account_4BDD06CE": { + } + }, + "aws_iam_policy_document": { + "testALB_iam-log-bucket-policy-document_C7E41154": { + "statement": [ + { + "actions": [ + "s3:PutObject" + ], + "effect": "Allow", + "principals": [ + { + "identifiers": [ + "arn:aws:iam::\${data.aws_elb_service_account.testALB_elb-service-account_4BDD06CE.id}:root" + ], + "type": "AWS" + } + ], + "resources": [ + "arn:aws:s3:::\${aws_s3_bucket.testALB_log-bucket_E9787BB1.bucket}/*" + ] + } + ] + } + } + }, + "resource": { + "aws_alb": { + "testALB_alb_F6B33218": { + "access_logs": { + "bucket": "\${aws_s3_bucket.testALB_log-bucket_E9787BB1.bucket}", + "enabled": true, + "prefix": "logs/ahoy/cool/service" + }, + "internal": false, + "name_prefix": "TEST", + "security_groups": [ + "\${aws_security_group.testALB_alb_security_group_57C45F23.id}" + ], + "subnets": [ + "a", + "b" + ], + "tags": { + "hobby": "bowling", + "name": "thedude" + } + } + }, + "aws_s3_bucket": { + "testALB_log-bucket_E9787BB1": { + "bucket": "logging-bucket", + "tags": { + "hobby": "bowling", + "name": "thedude" + } + } + }, + "aws_s3_bucket_policy": { + "testALB_log-bucket-policy_EB762FB5": { + "bucket": "\${aws_s3_bucket.testALB_log-bucket_E9787BB1.bucket}", + "policy": "\${data.aws_iam_policy_document.testALB_iam-log-bucket-policy-document_C7E41154.json}" + } + }, + "aws_security_group": { + "testALB_alb_security_group_57C45F23": { + "description": "External security group (Managed by Terraform)", + "egress": [ + { + "cidr_blocks": [ + "0.0.0.0/0" + ], + "description": "required", + "from_port": 0, + "ipv6_cidr_blocks": [ + ], + "prefix_list_ids": [ + ], + "protocol": "-1", + "security_groups": [ + ], + "self": null, + "to_port": 0 + } + ], + "ingress": [ + { + "cidr_blocks": [ + "0.0.0.0/0" + ], + "description": null, + "from_port": 443, + "ipv6_cidr_blocks": null, + "prefix_list_ids": null, + "protocol": "TCP", + "security_groups": null, + "self": null, + "to_port": 443 + }, + { + "cidr_blocks": [ + "0.0.0.0/0" + ], + "description": null, + "from_port": 80, + "ipv6_cidr_blocks": null, + "prefix_list_ids": null, + "protocol": "TCP", + "security_groups": null, + "self": null, + "to_port": 80 + } + ], + "lifecycle": { + "create_before_destroy": true + }, + "name_prefix": "test--HTTP/S Security Group", + "tags": { + "Name": "test--HTTP/S Security Group", + "hobby": "bowling", + "name": "thedude" + }, + "vpc_id": "123" + } + } + } +}" +`; + +exports[`ApplicationLoadBalancer renders an ALB with logs with an existing bucket 1`] = ` +"{ + "data": { + "aws_s3_bucket": { + "testALB_log-bucket_E9787BB1": { + "bucket": "logging-bucket" + } + } + }, + "resource": { + "aws_alb": { + "testALB_alb_F6B33218": { + "access_logs": { + "bucket": "\${data.aws_s3_bucket.testALB_log-bucket_E9787BB1.bucket}", + "enabled": true, + "prefix": "server-logs/test-/alb" + }, + "internal": false, + "name_prefix": "TEST", + "security_groups": [ + "\${aws_security_group.testALB_alb_security_group_57C45F23.id}" + ], + "subnets": [ + "a", + "b" + ], + "tags": { + "hobby": "bowling", + "name": "thedude" + } + } + }, + "aws_security_group": { + "testALB_alb_security_group_57C45F23": { + "description": "External security group (Managed by Terraform)", + "egress": [ + { + "cidr_blocks": [ + "0.0.0.0/0" + ], + "description": "required", + "from_port": 0, + "ipv6_cidr_blocks": [ + ], + "prefix_list_ids": [ + ], + "protocol": "-1", + "security_groups": [ + ], + "self": null, + "to_port": 0 + } + ], + "ingress": [ + { + "cidr_blocks": [ + "0.0.0.0/0" + ], + "description": null, + "from_port": 443, + "ipv6_cidr_blocks": null, + "prefix_list_ids": null, + "protocol": "TCP", + "security_groups": null, + "self": null, + "to_port": 443 + }, + { + "cidr_blocks": [ + "0.0.0.0/0" + ], + "description": null, + "from_port": 80, + "ipv6_cidr_blocks": null, + "prefix_list_ids": null, + "protocol": "TCP", + "security_groups": null, + "self": null, + "to_port": 80 + } + ], + "lifecycle": { + "create_before_destroy": true + }, + "name_prefix": "test--HTTP/S Security Group", + "tags": { + "Name": "test--HTTP/S Security Group", + "hobby": "bowling", + "name": "thedude" + }, + "vpc_id": "123" + } + } + } +}" +`; + +exports[`ApplicationLoadBalancer renders an ALB with logs with an existing bucket and prefix 1`] = ` +"{ + "data": { + "aws_s3_bucket": { + "testALB_log-bucket_E9787BB1": { + "bucket": "logging-bucket" + } + } + }, + "resource": { + "aws_alb": { + "testALB_alb_F6B33218": { + "access_logs": { + "bucket": "\${data.aws_s3_bucket.testALB_log-bucket_E9787BB1.bucket}", + "enabled": true, + "prefix": "logs/ahoy/cool/service" + }, + "internal": false, + "name_prefix": "TEST", + "security_groups": [ + "\${aws_security_group.testALB_alb_security_group_57C45F23.id}" + ], + "subnets": [ + "a", + "b" + ], + "tags": { + "hobby": "bowling", + "name": "thedude" + } + } + }, + "aws_security_group": { + "testALB_alb_security_group_57C45F23": { + "description": "External security group (Managed by Terraform)", + "egress": [ + { + "cidr_blocks": [ + "0.0.0.0/0" + ], + "description": "required", + "from_port": 0, + "ipv6_cidr_blocks": [ + ], + "prefix_list_ids": [ + ], + "protocol": "-1", + "security_groups": [ + ], + "self": null, + "to_port": 0 + } + ], + "ingress": [ + { + "cidr_blocks": [ + "0.0.0.0/0" + ], + "description": null, + "from_port": 443, + "ipv6_cidr_blocks": null, + "prefix_list_ids": null, + "protocol": "TCP", + "security_groups": null, + "self": null, + "to_port": 443 + }, + { + "cidr_blocks": [ + "0.0.0.0/0" + ], + "description": null, + "from_port": 80, + "ipv6_cidr_blocks": null, + "prefix_list_ids": null, + "protocol": "TCP", + "security_groups": null, + "self": null, + "to_port": 80 + } + ], + "lifecycle": { + "create_before_destroy": true + }, + "name_prefix": "test--HTTP/S Security Group", + "tags": { + "Name": "test--HTTP/S Security Group", + "hobby": "bowling", + "name": "thedude" + }, + "vpc_id": "123" + } + } + } +}" +`; + +exports[`ApplicationLoadBalancer renders an ALB with tags 1`] = ` +"{ + "resource": { + "aws_alb": { + "testALB_alb_F6B33218": { + "internal": false, + "name_prefix": "TEST", + "security_groups": [ + "\${aws_security_group.testALB_alb_security_group_57C45F23.id}" + ], + "subnets": [ + "a", + "b" + ], + "tags": { + "hobby": "bowling", + "name": "thedude" + } + } + }, + "aws_security_group": { + "testALB_alb_security_group_57C45F23": { + "description": "External security group (Managed by Terraform)", + "egress": [ + { + "cidr_blocks": [ + "0.0.0.0/0" + ], + "description": "required", + "from_port": 0, + "ipv6_cidr_blocks": [ + ], + "prefix_list_ids": [ + ], + "protocol": "-1", + "security_groups": [ + ], + "self": null, + "to_port": 0 + } + ], + "ingress": [ + { + "cidr_blocks": [ + "0.0.0.0/0" + ], + "description": null, + "from_port": 443, + "ipv6_cidr_blocks": null, + "prefix_list_ids": null, + "protocol": "TCP", + "security_groups": null, + "self": null, + "to_port": 443 + }, + { + "cidr_blocks": [ + "0.0.0.0/0" + ], + "description": null, + "from_port": 80, + "ipv6_cidr_blocks": null, + "prefix_list_ids": null, + "protocol": "TCP", + "security_groups": null, + "self": null, + "to_port": 80 + } + ], + "lifecycle": { + "create_before_destroy": true + }, + "name_prefix": "test--HTTP/S Security Group", + "tags": { + "Name": "test--HTTP/S Security Group", + "hobby": "bowling", + "name": "thedude" + }, + "vpc_id": "123" + } + } + } +}" +`; + +exports[`ApplicationLoadBalancer renders an ALB without tags 1`] = ` +"{ + "resource": { + "aws_alb": { + "testALB_alb_F6B33218": { + "internal": false, + "name_prefix": "TEST", + "security_groups": [ + "\${aws_security_group.testALB_alb_security_group_57C45F23.id}" + ], + "subnets": [ + "a", + "b" + ] + } + }, + "aws_security_group": { + "testALB_alb_security_group_57C45F23": { + "description": "External security group (Managed by Terraform)", + "egress": [ + { + "cidr_blocks": [ + "0.0.0.0/0" + ], + "description": "required", + "from_port": 0, + "ipv6_cidr_blocks": [ + ], + "prefix_list_ids": [ + ], + "protocol": "-1", + "security_groups": [ + ], + "self": null, + "to_port": 0 + } + ], + "ingress": [ + { + "cidr_blocks": [ + "0.0.0.0/0" + ], + "description": null, + "from_port": 443, + "ipv6_cidr_blocks": null, + "prefix_list_ids": null, + "protocol": "TCP", + "security_groups": null, + "self": null, + "to_port": 443 + }, + { + "cidr_blocks": [ + "0.0.0.0/0" + ], + "description": null, + "from_port": 80, + "ipv6_cidr_blocks": null, + "prefix_list_ids": null, + "protocol": "TCP", + "security_groups": null, + "self": null, + "to_port": 80 + } + ], + "lifecycle": { + "create_before_destroy": true + }, + "name_prefix": "test--HTTP/S Security Group", + "tags": { + "Name": "test--HTTP/S Security Group" + }, + "vpc_id": "123" + } + } + } +}" +`; diff --git a/packages/terraform-modules/src/base/__snapshots__/ApplicationMemcache.spec.ts.snap b/packages/terraform-modules/src/base/__snapshots__/ApplicationMemcache.spec.ts.snap new file mode 100644 index 00000000..7a09880b --- /dev/null +++ b/packages/terraform-modules/src/base/__snapshots__/ApplicationMemcache.spec.ts.snap @@ -0,0 +1,223 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ApplicationMemcache renders memcached with minimal config 1`] = ` +"{ + "data": { + "aws_vpc": { + "testMemcached_vpc_192C8AF1": { + "filter": [ + { + "name": "vpc-id", + "values": [ + "cool-vpc" + ] + } + ] + } + } + }, + "resource": { + "aws_elasticache_cluster": { + "testMemcached_elasticache_cluster_4853BD62": { + "apply_immediately": true, + "cluster_id": "abides-dev", + "depends_on": [ + "aws_elasticache_subnet_group.testMemcached_elasticache_subnet_group_609E6233", + "aws_security_group.testMemcached_elasticache_security_group_F23969B6" + ], + "engine": "memcached", + "engine_version": "1.6.6", + "node_type": "cache.t2.micro", + "num_cache_nodes": 2, + "parameter_group_name": "default.memcached1.6", + "port": 11211, + "security_group_ids": [ + "\${aws_security_group.testMemcached_elasticache_security_group_F23969B6.id}" + ], + "subnet_group_name": "\${aws_elasticache_subnet_group.testMemcached_elasticache_subnet_group_609E6233.name}" + } + }, + "aws_elasticache_subnet_group": { + "testMemcached_elasticache_subnet_group_609E6233": { + "name": "abides-dev-ElasticacheSubnetGroup", + "subnet_ids": [ + "1234-123" + ] + } + }, + "aws_security_group": { + "testMemcached_elasticache_security_group_F23969B6": { + "description": "Managed by Terraform", + "ingress": [ + { + "cidr_blocks": null, + "description": null, + "from_port": 11211, + "ipv6_cidr_blocks": null, + "prefix_list_ids": null, + "protocol": "tcp", + "security_groups": [ + ], + "self": null, + "to_port": 11211 + } + ], + "name_prefix": "abides-dev", + "vpc_id": "\${data.aws_vpc.testMemcached_vpc_192C8AF1.id}" + } + } + } +}" +`; + +exports[`ApplicationMemcache renders memcached with node change 1`] = ` +"{ + "data": { + "aws_vpc": { + "testMemcached_vpc_192C8AF1": { + "filter": [ + { + "name": "vpc-id", + "values": [ + "cool-vpc" + ] + } + ] + } + } + }, + "resource": { + "aws_elasticache_cluster": { + "testMemcached_elasticache_cluster_4853BD62": { + "apply_immediately": true, + "cluster_id": "abides-dev", + "depends_on": [ + "aws_elasticache_subnet_group.testMemcached_elasticache_subnet_group_609E6233", + "aws_security_group.testMemcached_elasticache_security_group_F23969B6" + ], + "engine": "memcached", + "engine_version": "1.6.6", + "node_type": "cache.m4.2xlarge", + "num_cache_nodes": 5, + "parameter_group_name": "default.memcached1.6", + "port": 11211, + "security_group_ids": [ + "\${aws_security_group.testMemcached_elasticache_security_group_F23969B6.id}" + ], + "subnet_group_name": "\${aws_elasticache_subnet_group.testMemcached_elasticache_subnet_group_609E6233.name}" + } + }, + "aws_elasticache_subnet_group": { + "testMemcached_elasticache_subnet_group_609E6233": { + "name": "abides-dev-ElasticacheSubnetGroup", + "subnet_ids": [ + "1234-123" + ] + } + }, + "aws_security_group": { + "testMemcached_elasticache_security_group_F23969B6": { + "description": "Managed by Terraform", + "ingress": [ + { + "cidr_blocks": null, + "description": null, + "from_port": 11211, + "ipv6_cidr_blocks": null, + "prefix_list_ids": null, + "protocol": "tcp", + "security_groups": [ + ], + "self": null, + "to_port": 11211 + } + ], + "name_prefix": "abides-dev", + "vpc_id": "\${data.aws_vpc.testMemcached_vpc_192C8AF1.id}" + } + } + } +}" +`; + +exports[`ApplicationMemcache renders memcached with tags 1`] = ` +"{ + "data": { + "aws_vpc": { + "testMemcached_vpc_192C8AF1": { + "filter": [ + { + "name": "vpc-id", + "values": [ + "cool-vpc" + ] + } + ] + } + } + }, + "resource": { + "aws_elasticache_cluster": { + "testMemcached_elasticache_cluster_4853BD62": { + "apply_immediately": true, + "cluster_id": "abides-dev", + "depends_on": [ + "aws_elasticache_subnet_group.testMemcached_elasticache_subnet_group_609E6233", + "aws_security_group.testMemcached_elasticache_security_group_F23969B6" + ], + "engine": "memcached", + "engine_version": "1.6.6", + "node_type": "cache.t2.micro", + "num_cache_nodes": 2, + "parameter_group_name": "default.memcached1.6", + "port": 11211, + "security_group_ids": [ + "\${aws_security_group.testMemcached_elasticache_security_group_F23969B6.id}" + ], + "subnet_group_name": "\${aws_elasticache_subnet_group.testMemcached_elasticache_subnet_group_609E6233.name}", + "tags": { + "donnie": "throwinrockstonight", + "letsgo": "bowling" + } + } + }, + "aws_elasticache_subnet_group": { + "testMemcached_elasticache_subnet_group_609E6233": { + "name": "abides-dev-ElasticacheSubnetGroup", + "subnet_ids": [ + "1234-123" + ], + "tags": { + "donnie": "throwinrockstonight", + "letsgo": "bowling" + } + } + }, + "aws_security_group": { + "testMemcached_elasticache_security_group_F23969B6": { + "description": "Managed by Terraform", + "ingress": [ + { + "cidr_blocks": null, + "description": null, + "from_port": 11211, + "ipv6_cidr_blocks": null, + "prefix_list_ids": null, + "protocol": "tcp", + "security_groups": [ + ], + "self": null, + "to_port": 11211 + } + ], + "name_prefix": "abides-dev", + "tags": { + "donnie": "throwinrockstonight", + "letsgo": "bowling" + }, + "vpc_id": "\${data.aws_vpc.testMemcached_vpc_192C8AF1.id}" + } + } + } +}" +`; diff --git a/packages/terraform-modules/src/base/__snapshots__/ApplicationRDSCluster.spec.ts.snap b/packages/terraform-modules/src/base/__snapshots__/ApplicationRDSCluster.spec.ts.snap new file mode 100644 index 00000000..0be4ab15 --- /dev/null +++ b/packages/terraform-modules/src/base/__snapshots__/ApplicationRDSCluster.spec.ts.snap @@ -0,0 +1,359 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ApplicationRDSCluster renders a RDS cluster 1`] = ` +"{ + "data": { + "aws_vpc": { + "testRDSCluster_vpc_F47EEEFE": { + "filter": [ + { + "name": "vpc-id", + "values": [ + "rug" + ] + } + ] + } + } + }, + "resource": { + "aws_db_subnet_group": { + "testRDSCluster_rds_subnet_group_88022457": { + "name_prefix": "bowling-", + "subnet_ids": [ + "0", + "1" + ], + "tags": { + "whodis": "walter" + } + } + }, + "aws_rds_cluster": { + "testRDSCluster_rds_cluster_B5FD08B5": { + "cluster_identifier_prefix": "bowling-", + "copy_tags_to_snapshot": true, + "database_name": "walter", + "db_subnet_group_name": "\${aws_db_subnet_group.testRDSCluster_rds_subnet_group_88022457.name}", + "engine": "aurora-mysql", + "lifecycle": { + "ignore_changes": [ + "master_username", + "master_password" + ] + }, + "master_password": "bowling", + "master_username": "walter", + "tags": { + "whodis": "walter" + }, + "vpc_security_group_ids": [ + "\${aws_security_group.testRDSCluster_rds_security_group_4A9D257E.id}" + ] + } + }, + "aws_secretsmanager_secret": { + "testRDSCluster_rds_secret_A2014138": { + "depends_on": [ + "aws_rds_cluster.testRDSCluster_rds_cluster_B5FD08B5" + ], + "description": "Secret For \${aws_rds_cluster.testRDSCluster_rds_cluster_B5FD08B5.cluster_identifier}", + "name": "bowling-/\${aws_rds_cluster.testRDSCluster_rds_cluster_B5FD08B5.cluster_identifier}", + "tags": { + "whodis": "walter" + } + } + }, + "aws_secretsmanager_secret_version": { + "testRDSCluster_rds_secret_version_55C44893": { + "depends_on": [ + "aws_secretsmanager_secret.testRDSCluster_rds_secret_A2014138" + ], + "secret_id": "\${aws_secretsmanager_secret.testRDSCluster_rds_secret_A2014138.id}", + "secret_string": "{\\"engine\\":\\"aurora-mysql\\",\\"host\\":\\"\${aws_rds_cluster.testRDSCluster_rds_cluster_B5FD08B5.endpoint}\\",\\"username\\":\\"\${aws_rds_cluster.testRDSCluster_rds_cluster_B5FD08B5.master_username}\\",\\"password\\":\\"\${aws_rds_cluster.testRDSCluster_rds_cluster_B5FD08B5.master_password}\\",\\"dbname\\":\\"\${aws_rds_cluster.testRDSCluster_rds_cluster_B5FD08B5.database_name}\\",\\"port\\":3306,\\"database_url\\":\\"mysql://\${aws_rds_cluster.testRDSCluster_rds_cluster_B5FD08B5.master_username}:\${aws_rds_cluster.testRDSCluster_rds_cluster_B5FD08B5.master_password}@\${aws_rds_cluster.testRDSCluster_rds_cluster_B5FD08B5.endpoint}:3306/\${aws_rds_cluster.testRDSCluster_rds_cluster_B5FD08B5.database_name}\\"}" + } + }, + "aws_security_group": { + "testRDSCluster_rds_security_group_4A9D257E": { + "description": "Managed by Terraform", + "egress": [ + { + "cidr_blocks": [ + "0.0.0.0/0" + ], + "description": "required", + "from_port": 0, + "ipv6_cidr_blocks": [ + ], + "prefix_list_ids": [ + ], + "protocol": "-1", + "security_groups": [ + ], + "self": null, + "to_port": 0 + } + ], + "ingress": [ + { + "cidr_blocks": [ + "\${data.aws_vpc.testRDSCluster_vpc_F47EEEFE.cidr_block}" + ], + "description": null, + "from_port": 3306, + "ipv6_cidr_blocks": null, + "prefix_list_ids": null, + "protocol": "tcp", + "security_groups": null, + "self": null, + "to_port": 3306 + } + ], + "name_prefix": "bowling-", + "tags": { + "whodis": "walter" + }, + "vpc_id": "\${data.aws_vpc.testRDSCluster_vpc_F47EEEFE.id}" + } + } + } +}" +`; + +exports[`ApplicationRDSCluster renders a RDS cluster with a database URL 1`] = ` +"{ + "data": { + "aws_vpc": { + "testRDSCluster_vpc_F47EEEFE": { + "filter": [ + { + "name": "vpc-id", + "values": [ + "rug" + ] + } + ] + } + } + }, + "resource": { + "aws_db_subnet_group": { + "testRDSCluster_rds_subnet_group_88022457": { + "name_prefix": "bowling-", + "subnet_ids": [ + "0", + "1" + ], + "tags": { + "whodis": "walter" + } + } + }, + "aws_rds_cluster": { + "testRDSCluster_rds_cluster_B5FD08B5": { + "cluster_identifier_prefix": "bowling-", + "copy_tags_to_snapshot": true, + "database_name": "walter", + "db_subnet_group_name": "\${aws_db_subnet_group.testRDSCluster_rds_subnet_group_88022457.name}", + "engine": "aurora-mysql", + "lifecycle": { + "ignore_changes": [ + "master_username", + "master_password" + ] + }, + "master_password": "bowling", + "master_username": "walter", + "tags": { + "whodis": "walter" + }, + "vpc_security_group_ids": [ + "\${aws_security_group.testRDSCluster_rds_security_group_4A9D257E.id}" + ] + } + }, + "aws_secretsmanager_secret": { + "testRDSCluster_rds_secret_A2014138": { + "depends_on": [ + "aws_rds_cluster.testRDSCluster_rds_cluster_B5FD08B5" + ], + "description": "Secret For \${aws_rds_cluster.testRDSCluster_rds_cluster_B5FD08B5.cluster_identifier}", + "name": "bowling-/\${aws_rds_cluster.testRDSCluster_rds_cluster_B5FD08B5.cluster_identifier}", + "tags": { + "whodis": "walter" + } + } + }, + "aws_secretsmanager_secret_version": { + "testRDSCluster_rds_secret_version_55C44893": { + "depends_on": [ + "aws_secretsmanager_secret.testRDSCluster_rds_secret_A2014138" + ], + "secret_id": "\${aws_secretsmanager_secret.testRDSCluster_rds_secret_A2014138.id}", + "secret_string": "{\\"engine\\":\\"aurora-mysql\\",\\"host\\":\\"\${aws_rds_cluster.testRDSCluster_rds_cluster_B5FD08B5.endpoint}\\",\\"username\\":\\"\${aws_rds_cluster.testRDSCluster_rds_cluster_B5FD08B5.master_username}\\",\\"password\\":\\"\${aws_rds_cluster.testRDSCluster_rds_cluster_B5FD08B5.master_password}\\",\\"dbname\\":\\"\${aws_rds_cluster.testRDSCluster_rds_cluster_B5FD08B5.database_name}\\",\\"port\\":3306,\\"database_url\\":\\"mysql://\${aws_rds_cluster.testRDSCluster_rds_cluster_B5FD08B5.master_username}:\${aws_rds_cluster.testRDSCluster_rds_cluster_B5FD08B5.master_password}@\${aws_rds_cluster.testRDSCluster_rds_cluster_B5FD08B5.endpoint}:3306/\${aws_rds_cluster.testRDSCluster_rds_cluster_B5FD08B5.database_name}\\"}" + } + }, + "aws_security_group": { + "testRDSCluster_rds_security_group_4A9D257E": { + "description": "Managed by Terraform", + "egress": [ + { + "cidr_blocks": [ + "0.0.0.0/0" + ], + "description": "required", + "from_port": 0, + "ipv6_cidr_blocks": [ + ], + "prefix_list_ids": [ + ], + "protocol": "-1", + "security_groups": [ + ], + "self": null, + "to_port": 0 + } + ], + "ingress": [ + { + "cidr_blocks": [ + "\${data.aws_vpc.testRDSCluster_vpc_F47EEEFE.cidr_block}" + ], + "description": null, + "from_port": 3306, + "ipv6_cidr_blocks": null, + "prefix_list_ids": null, + "protocol": "tcp", + "security_groups": null, + "self": null, + "to_port": 3306 + } + ], + "name_prefix": "bowling-", + "tags": { + "whodis": "walter" + }, + "vpc_id": "\${data.aws_vpc.testRDSCluster_vpc_F47EEEFE.id}" + } + } + } +}" +`; + +exports[`ApplicationRDSCluster renders a RDS cluster without a name 1`] = ` +"{ + "data": { + "aws_vpc": { + "testRDSCluster_vpc_F47EEEFE": { + "filter": [ + { + "name": "vpc-id", + "values": [ + "rug" + ] + } + ] + } + } + }, + "resource": { + "aws_db_subnet_group": { + "testRDSCluster_rds_subnet_group_88022457": { + "subnet_ids": [ + "0", + "1" + ], + "tags": { + "whodis": "walter" + } + } + }, + "aws_rds_cluster": { + "testRDSCluster_rds_cluster_B5FD08B5": { + "copy_tags_to_snapshot": true, + "database_name": "walter", + "db_subnet_group_name": "\${aws_db_subnet_group.testRDSCluster_rds_subnet_group_88022457.name}", + "engine": "aurora-mysql", + "lifecycle": { + "ignore_changes": [ + "master_username", + "master_password" + ] + }, + "master_password": "bowling", + "master_username": "walter", + "tags": { + "whodis": "walter" + }, + "vpc_security_group_ids": [ + "\${aws_security_group.testRDSCluster_rds_security_group_4A9D257E.id}" + ] + } + }, + "aws_secretsmanager_secret": { + "testRDSCluster_rds_secret_A2014138": { + "depends_on": [ + "aws_rds_cluster.testRDSCluster_rds_cluster_B5FD08B5" + ], + "description": "Secret For \${aws_rds_cluster.testRDSCluster_rds_cluster_B5FD08B5.cluster_identifier}", + "name": "bowling-/\${aws_rds_cluster.testRDSCluster_rds_cluster_B5FD08B5.cluster_identifier}", + "tags": { + "whodis": "walter" + } + } + }, + "aws_secretsmanager_secret_version": { + "testRDSCluster_rds_secret_version_55C44893": { + "depends_on": [ + "aws_secretsmanager_secret.testRDSCluster_rds_secret_A2014138" + ], + "secret_id": "\${aws_secretsmanager_secret.testRDSCluster_rds_secret_A2014138.id}", + "secret_string": "{\\"engine\\":\\"aurora-mysql\\",\\"host\\":\\"\${aws_rds_cluster.testRDSCluster_rds_cluster_B5FD08B5.endpoint}\\",\\"username\\":\\"\${aws_rds_cluster.testRDSCluster_rds_cluster_B5FD08B5.master_username}\\",\\"password\\":\\"\${aws_rds_cluster.testRDSCluster_rds_cluster_B5FD08B5.master_password}\\",\\"dbname\\":\\"\${aws_rds_cluster.testRDSCluster_rds_cluster_B5FD08B5.database_name}\\",\\"port\\":3306,\\"database_url\\":\\"mysql://\${aws_rds_cluster.testRDSCluster_rds_cluster_B5FD08B5.master_username}:\${aws_rds_cluster.testRDSCluster_rds_cluster_B5FD08B5.master_password}@\${aws_rds_cluster.testRDSCluster_rds_cluster_B5FD08B5.endpoint}:3306/\${aws_rds_cluster.testRDSCluster_rds_cluster_B5FD08B5.database_name}\\"}" + } + }, + "aws_security_group": { + "testRDSCluster_rds_security_group_4A9D257E": { + "description": "Managed by Terraform", + "egress": [ + { + "cidr_blocks": [ + "0.0.0.0/0" + ], + "description": "required", + "from_port": 0, + "ipv6_cidr_blocks": [ + ], + "prefix_list_ids": [ + ], + "protocol": "-1", + "security_groups": [ + ], + "self": null, + "to_port": 0 + } + ], + "ingress": [ + { + "cidr_blocks": [ + "\${data.aws_vpc.testRDSCluster_vpc_F47EEEFE.cidr_block}" + ], + "description": null, + "from_port": 3306, + "ipv6_cidr_blocks": null, + "prefix_list_ids": null, + "protocol": "tcp", + "security_groups": null, + "self": null, + "to_port": 3306 + } + ], + "name_prefix": "bowling-", + "tags": { + "whodis": "walter" + }, + "vpc_id": "\${data.aws_vpc.testRDSCluster_vpc_F47EEEFE.id}" + } + } + } +}" +`; diff --git a/packages/terraform-modules/src/base/__snapshots__/ApplicationRedis.spec.ts.snap b/packages/terraform-modules/src/base/__snapshots__/ApplicationRedis.spec.ts.snap new file mode 100644 index 00000000..d1918e52 --- /dev/null +++ b/packages/terraform-modules/src/base/__snapshots__/ApplicationRedis.spec.ts.snap @@ -0,0 +1,229 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ApplicationRedis renders redis with minimal config 1`] = ` +"{ + "data": { + "aws_vpc": { + "testRedis_vpc_442CAD26": { + "filter": [ + { + "name": "vpc-id", + "values": [ + "cool-vpc" + ] + } + ] + } + } + }, + "resource": { + "aws_elasticache_replication_group": { + "testRedis_elasticache_replication_group_E03AAD61": { + "apply_immediately": true, + "automatic_failover_enabled": true, + "depends_on": [ + "aws_elasticache_subnet_group.testRedis_elasticache_subnet_group_389F6AE7", + "aws_security_group.testRedis_elasticache_security_group_36C6CB6D" + ], + "description": "abides-dev | Managed by terraform", + "engine_version": "7.0", + "multi_az_enabled": true, + "node_type": "cache.t3.micro", + "num_cache_clusters": 2, + "parameter_group_name": "default.redis7", + "port": 6379, + "replication_group_id": "abides-dev", + "security_group_ids": [ + "\${aws_security_group.testRedis_elasticache_security_group_36C6CB6D.id}" + ], + "subnet_group_name": "\${aws_elasticache_subnet_group.testRedis_elasticache_subnet_group_389F6AE7.name}" + } + }, + "aws_elasticache_subnet_group": { + "testRedis_elasticache_subnet_group_389F6AE7": { + "name": "abides-dev-ElasticacheSubnetGroup", + "subnet_ids": [ + "1234-123" + ] + } + }, + "aws_security_group": { + "testRedis_elasticache_security_group_36C6CB6D": { + "description": "Managed by Terraform", + "ingress": [ + { + "cidr_blocks": null, + "description": null, + "from_port": 6379, + "ipv6_cidr_blocks": null, + "prefix_list_ids": null, + "protocol": "tcp", + "security_groups": [ + ], + "self": null, + "to_port": 6379 + } + ], + "name_prefix": "abides-dev", + "vpc_id": "\${data.aws_vpc.testRedis_vpc_442CAD26.id}" + } + } + } +}" +`; + +exports[`ApplicationRedis renders redis with node change 1`] = ` +"{ + "data": { + "aws_vpc": { + "testRedis_vpc_442CAD26": { + "filter": [ + { + "name": "vpc-id", + "values": [ + "cool-vpc" + ] + } + ] + } + } + }, + "resource": { + "aws_elasticache_replication_group": { + "testRedis_elasticache_replication_group_E03AAD61": { + "apply_immediately": true, + "automatic_failover_enabled": true, + "depends_on": [ + "aws_elasticache_subnet_group.testRedis_elasticache_subnet_group_389F6AE7", + "aws_security_group.testRedis_elasticache_security_group_36C6CB6D" + ], + "description": "abides-dev | Managed by terraform", + "engine_version": "7.0", + "multi_az_enabled": true, + "node_type": "cache.m4.2xlarge", + "num_cache_clusters": 5, + "parameter_group_name": "default.redis7", + "port": 6379, + "replication_group_id": "abides-dev", + "security_group_ids": [ + "\${aws_security_group.testRedis_elasticache_security_group_36C6CB6D.id}" + ], + "subnet_group_name": "\${aws_elasticache_subnet_group.testRedis_elasticache_subnet_group_389F6AE7.name}" + } + }, + "aws_elasticache_subnet_group": { + "testRedis_elasticache_subnet_group_389F6AE7": { + "name": "abides-dev-ElasticacheSubnetGroup", + "subnet_ids": [ + "1234-123" + ] + } + }, + "aws_security_group": { + "testRedis_elasticache_security_group_36C6CB6D": { + "description": "Managed by Terraform", + "ingress": [ + { + "cidr_blocks": null, + "description": null, + "from_port": 6379, + "ipv6_cidr_blocks": null, + "prefix_list_ids": null, + "protocol": "tcp", + "security_groups": [ + ], + "self": null, + "to_port": 6379 + } + ], + "name_prefix": "abides-dev", + "vpc_id": "\${data.aws_vpc.testRedis_vpc_442CAD26.id}" + } + } + } +}" +`; + +exports[`ApplicationRedis renders redis with tags 1`] = ` +"{ + "data": { + "aws_vpc": { + "testRedis_vpc_442CAD26": { + "filter": [ + { + "name": "vpc-id", + "values": [ + "cool-vpc" + ] + } + ] + } + } + }, + "resource": { + "aws_elasticache_replication_group": { + "testRedis_elasticache_replication_group_E03AAD61": { + "apply_immediately": true, + "automatic_failover_enabled": true, + "depends_on": [ + "aws_elasticache_subnet_group.testRedis_elasticache_subnet_group_389F6AE7", + "aws_security_group.testRedis_elasticache_security_group_36C6CB6D" + ], + "description": "abides-dev | Managed by terraform", + "engine_version": "7.0", + "multi_az_enabled": true, + "node_type": "cache.t3.micro", + "num_cache_clusters": 2, + "parameter_group_name": "default.redis7", + "port": 6379, + "replication_group_id": "abides-dev", + "security_group_ids": [ + "\${aws_security_group.testRedis_elasticache_security_group_36C6CB6D.id}" + ], + "subnet_group_name": "\${aws_elasticache_subnet_group.testRedis_elasticache_subnet_group_389F6AE7.name}", + "tags": { + "donnie": "throwinrockstonight", + "letsgo": "bowling" + } + } + }, + "aws_elasticache_subnet_group": { + "testRedis_elasticache_subnet_group_389F6AE7": { + "name": "abides-dev-ElasticacheSubnetGroup", + "subnet_ids": [ + "1234-123" + ], + "tags": { + "donnie": "throwinrockstonight", + "letsgo": "bowling" + } + } + }, + "aws_security_group": { + "testRedis_elasticache_security_group_36C6CB6D": { + "description": "Managed by Terraform", + "ingress": [ + { + "cidr_blocks": null, + "description": null, + "from_port": 6379, + "ipv6_cidr_blocks": null, + "prefix_list_ids": null, + "protocol": "tcp", + "security_groups": [ + ], + "self": null, + "to_port": 6379 + } + ], + "name_prefix": "abides-dev", + "tags": { + "donnie": "throwinrockstonight", + "letsgo": "bowling" + }, + "vpc_id": "\${data.aws_vpc.testRedis_vpc_442CAD26.id}" + } + } + } +}" +`; diff --git a/packages/terraform-modules/src/base/__snapshots__/ApplicationSQSQueue.spec.ts.snap b/packages/terraform-modules/src/base/__snapshots__/ApplicationSQSQueue.spec.ts.snap new file mode 100644 index 00000000..6262061d --- /dev/null +++ b/packages/terraform-modules/src/base/__snapshots__/ApplicationSQSQueue.spec.ts.snap @@ -0,0 +1,45 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ApplicationSQSQueue renders an sqs queue with max message size 1`] = ` +"{ + "resource": { + "aws_sqs_queue": { + "testSQS_sqs_queue_B45AAD64": { + "fifo_queue": false, + "max_message_size": 86753, + "name": "TEST" + } + } + } +}" +`; + +exports[`ApplicationSQSQueue renders an sqs queue with tags 1`] = ` +"{ + "resource": { + "aws_sqs_queue": { + "testSQS_sqs_queue_B45AAD64": { + "fifo_queue": false, + "name": "TEST", + "tags": { + "environment": "prod", + "test": "123" + } + } + } + } +}" +`; + +exports[`ApplicationSQSQueue renders an sqs queue without tags 1`] = ` +"{ + "resource": { + "aws_sqs_queue": { + "testSQS_sqs_queue_B45AAD64": { + "fifo_queue": false, + "name": "TEST" + } + } + } +}" +`; diff --git a/packages/terraform-modules/src/base/__snapshots__/ApplicationSqsSnsTopicSubscription.spec.ts.snap b/packages/terraform-modules/src/base/__snapshots__/ApplicationSqsSnsTopicSubscription.spec.ts.snap new file mode 100644 index 00000000..ee4bde29 --- /dev/null +++ b/packages/terraform-modules/src/base/__snapshots__/ApplicationSqsSnsTopicSubscription.spec.ts.snap @@ -0,0 +1,322 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ApplicationSqsSnsTopicSubscription renders an SQS SNS subscription witg dlq passed 1`] = ` +"{ + "data": { + "aws_iam_policy_document": { + "sqs-sns-subscription_sns-dlq-policy-document_46022E28": { + "depends_on": [ + "aws_sqs_queue.dlq" + ], + "statement": [ + { + "actions": [ + "sqs:SendMessage" + ], + "condition": [ + { + "test": "ArnEquals", + "values": [ + "arn:aws:sns:TopicName" + ], + "variable": "aws:SourceArn" + } + ], + "effect": "Allow", + "principals": [ + { + "identifiers": [ + "sns.amazonaws.com" + ], + "type": "Service" + } + ], + "resources": [ + "\${aws_sqs_queue.dlq.arn}" + ] + } + ] + }, + "sqs-sns-subscription_sns-sqs-policy-document_ABFC60AA": { + "depends_on": [ + "aws_sqs_queue.sqs" + ], + "statement": [ + { + "actions": [ + "sqs:SendMessage" + ], + "condition": [ + { + "test": "ArnEquals", + "values": [ + "arn:aws:sns:TopicName" + ], + "variable": "aws:SourceArn" + } + ], + "effect": "Allow", + "principals": [ + { + "identifiers": [ + "sns.amazonaws.com" + ], + "type": "Service" + } + ], + "resources": [ + "\${aws_sqs_queue.sqs.arn}" + ] + } + ] + } + } + }, + "resource": { + "aws_sns_topic_subscription": { + "sqs-sns-subscription_5757EDDD": { + "depends_on": [ + "aws_sqs_queue.dlq" + ], + "endpoint": "\${aws_sqs_queue.sqs.arn}", + "protocol": "sqs", + "redrive_policy": "{\\"deadLetterTargetArn\\":\\"\${aws_sqs_queue.dlq.arn}\\"}", + "topic_arn": "arn:aws:sns:TopicName" + } + }, + "aws_sqs_queue": { + "dlq": { + "name": "test-sqs-dlq" + }, + "sqs": { + "name": "test-sqs" + } + }, + "aws_sqs_queue_policy": { + "sqs-sns-subscription_sns-dlq-policy_736EBF83": { + "policy": "\${data.aws_iam_policy_document.sqs-sns-subscription_sns-dlq-policy-document_46022E28.json}", + "queue_url": "\${aws_sqs_queue.dlq.url}" + }, + "sqs-sns-subscription_sns-sqs-policy_ADC74422": { + "policy": "\${data.aws_iam_policy_document.sqs-sns-subscription_sns-sqs-policy-document_ABFC60AA.json}", + "queue_url": "\${aws_sqs_queue.sqs.url}" + } + } + } +}" +`; + +exports[`ApplicationSqsSnsTopicSubscription renders an SQS SNS subscription with tags 1`] = ` +"{ + "data": { + "aws_iam_policy_document": { + "sqs-sns-subscription_sns-dlq-policy-document_46022E28": { + "depends_on": [ + "aws_sqs_queue.sqs-sns-subscription_sns-topic-dql_23368678" + ], + "statement": [ + { + "actions": [ + "sqs:SendMessage" + ], + "condition": [ + { + "test": "ArnEquals", + "values": [ + "arn:aws:sns:TopicName" + ], + "variable": "aws:SourceArn" + } + ], + "effect": "Allow", + "principals": [ + { + "identifiers": [ + "sns.amazonaws.com" + ], + "type": "Service" + } + ], + "resources": [ + "\${aws_sqs_queue.sqs-sns-subscription_sns-topic-dql_23368678.arn}" + ] + } + ] + }, + "sqs-sns-subscription_sns-sqs-policy-document_ABFC60AA": { + "depends_on": [ + "aws_sqs_queue.sqs" + ], + "statement": [ + { + "actions": [ + "sqs:SendMessage" + ], + "condition": [ + { + "test": "ArnEquals", + "values": [ + "arn:aws:sns:TopicName" + ], + "variable": "aws:SourceArn" + } + ], + "effect": "Allow", + "principals": [ + { + "identifiers": [ + "sns.amazonaws.com" + ], + "type": "Service" + } + ], + "resources": [ + "\${aws_sqs_queue.sqs.arn}" + ] + } + ] + } + } + }, + "resource": { + "aws_sns_topic_subscription": { + "sqs-sns-subscription_5757EDDD": { + "depends_on": [ + "aws_sqs_queue.sqs-sns-subscription_sns-topic-dql_23368678" + ], + "endpoint": "\${aws_sqs_queue.sqs.arn}", + "protocol": "sqs", + "redrive_policy": "{\\"deadLetterTargetArn\\":\\"\${aws_sqs_queue.sqs-sns-subscription_sns-topic-dql_23368678.arn}\\"}", + "topic_arn": "arn:aws:sns:TopicName" + } + }, + "aws_sqs_queue": { + "sqs": { + "name": "test-sqs" + }, + "sqs-sns-subscription_sns-topic-dql_23368678": { + "name": "test-sns-subscription-SNS-Topic-DLQ", + "tags": { + "hello": "there" + } + } + }, + "aws_sqs_queue_policy": { + "sqs-sns-subscription_sns-dlq-policy_736EBF83": { + "policy": "\${data.aws_iam_policy_document.sqs-sns-subscription_sns-dlq-policy-document_46022E28.json}", + "queue_url": "\${aws_sqs_queue.sqs-sns-subscription_sns-topic-dql_23368678.url}" + }, + "sqs-sns-subscription_sns-sqs-policy_ADC74422": { + "policy": "\${data.aws_iam_policy_document.sqs-sns-subscription_sns-sqs-policy-document_ABFC60AA.json}", + "queue_url": "\${aws_sqs_queue.sqs.url}" + } + } + } +}" +`; + +exports[`ApplicationSqsSnsTopicSubscription renders an SQS SNS subscription without tags 1`] = ` +"{ + "data": { + "aws_iam_policy_document": { + "sqs-sns-subscription_sns-dlq-policy-document_46022E28": { + "depends_on": [ + "aws_sqs_queue.sqs-sns-subscription_sns-topic-dql_23368678" + ], + "statement": [ + { + "actions": [ + "sqs:SendMessage" + ], + "condition": [ + { + "test": "ArnEquals", + "values": [ + "arn:aws:sns:TopicName" + ], + "variable": "aws:SourceArn" + } + ], + "effect": "Allow", + "principals": [ + { + "identifiers": [ + "sns.amazonaws.com" + ], + "type": "Service" + } + ], + "resources": [ + "\${aws_sqs_queue.sqs-sns-subscription_sns-topic-dql_23368678.arn}" + ] + } + ] + }, + "sqs-sns-subscription_sns-sqs-policy-document_ABFC60AA": { + "depends_on": [ + "aws_sqs_queue.sqs" + ], + "statement": [ + { + "actions": [ + "sqs:SendMessage" + ], + "condition": [ + { + "test": "ArnEquals", + "values": [ + "arn:aws:sns:TopicName" + ], + "variable": "aws:SourceArn" + } + ], + "effect": "Allow", + "principals": [ + { + "identifiers": [ + "sns.amazonaws.com" + ], + "type": "Service" + } + ], + "resources": [ + "\${aws_sqs_queue.sqs.arn}" + ] + } + ] + } + } + }, + "resource": { + "aws_sns_topic_subscription": { + "sqs-sns-subscription_5757EDDD": { + "depends_on": [ + "aws_sqs_queue.sqs-sns-subscription_sns-topic-dql_23368678" + ], + "endpoint": "\${aws_sqs_queue.sqs.arn}", + "protocol": "sqs", + "redrive_policy": "{\\"deadLetterTargetArn\\":\\"\${aws_sqs_queue.sqs-sns-subscription_sns-topic-dql_23368678.arn}\\"}", + "topic_arn": "arn:aws:sns:TopicName" + } + }, + "aws_sqs_queue": { + "sqs": { + "name": "test-sqs" + }, + "sqs-sns-subscription_sns-topic-dql_23368678": { + "name": "test-sns-subscription-SNS-Topic-DLQ" + } + }, + "aws_sqs_queue_policy": { + "sqs-sns-subscription_sns-dlq-policy_736EBF83": { + "policy": "\${data.aws_iam_policy_document.sqs-sns-subscription_sns-dlq-policy-document_46022E28.json}", + "queue_url": "\${aws_sqs_queue.sqs-sns-subscription_sns-topic-dql_23368678.url}" + }, + "sqs-sns-subscription_sns-sqs-policy_ADC74422": { + "policy": "\${data.aws_iam_policy_document.sqs-sns-subscription_sns-sqs-policy-document_ABFC60AA.json}", + "queue_url": "\${aws_sqs_queue.sqs.url}" + } + } + } +}" +`; diff --git a/packages/terraform-modules/src/base/__snapshots__/ApplicationTargetGroup.spec.ts.snap b/packages/terraform-modules/src/base/__snapshots__/ApplicationTargetGroup.spec.ts.snap new file mode 100644 index 00000000..cf540168 --- /dev/null +++ b/packages/terraform-modules/src/base/__snapshots__/ApplicationTargetGroup.spec.ts.snap @@ -0,0 +1,53 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ApplicationTargetGroup renders a Target Group with tags 1`] = ` +"{ + "resource": { + "aws_alb_target_group": { + "testTargetGroup_ecs_target_group_A0C76C7F": { + "deregistration_delay": "120", + "health_check": { + "healthy_threshold": 5, + "interval": 15, + "path": "/", + "protocol": "HTTP", + "unhealthy_threshold": 3 + }, + "name_prefix": "A1BC", + "port": 80, + "protocol": "HTTP", + "tags": { + "hobby": "bowling", + "name": "thedude" + }, + "target_type": "ip", + "vpc_id": "123" + } + } + } +}" +`; + +exports[`ApplicationTargetGroup renders a Target Group without tags 1`] = ` +"{ + "resource": { + "aws_alb_target_group": { + "testTargetGroup_ecs_target_group_A0C76C7F": { + "deregistration_delay": "120", + "health_check": { + "healthy_threshold": 5, + "interval": 15, + "path": "/", + "protocol": "HTTP", + "unhealthy_threshold": 3 + }, + "name_prefix": "ABC", + "port": 80, + "protocol": "HTTP", + "target_type": "ip", + "vpc_id": "123" + } + } + } +}" +`; diff --git a/packages/terraform-modules/src/base/__snapshots__/ApplicationVersionedLambda.spec.ts.snap b/packages/terraform-modules/src/base/__snapshots__/ApplicationVersionedLambda.spec.ts.snap new file mode 100644 index 00000000..85b48cb1 --- /dev/null +++ b/packages/terraform-modules/src/base/__snapshots__/ApplicationVersionedLambda.spec.ts.snap @@ -0,0 +1,1776 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ApplicationVersionedLambda renders a lambda with a node v14 runtime 1`] = ` +"{ + "data": { + "archive_file": { + "test-versioned-lambda_lambda-default-file_E2490368": { + "output_path": "index.js.zip", + "source": [ + { + "content": "exports.handler = (event, context) => { console.log(event) }", + "filename": "index.js" + } + ], + "type": "zip" + } + }, + "aws_iam_policy_document": { + "test-versioned-lambda_assume-policy-document_54E2E6E3": { + "statement": [ + { + "actions": [ + "sts:AssumeRole" + ], + "effect": "Allow", + "principals": [ + { + "identifiers": [ + "lambda.amazonaws.com", + "edgelambda.amazonaws.com" + ], + "type": "Service" + } + ] + } + ], + "version": "2012-10-17" + }, + "test-versioned-lambda_execution-policy-document_BA37CD4D": { + "statement": [ + { + "actions": [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents", + "logs:DescribeLogStreams" + ], + "effect": "Allow", + "resources": [ + "arn:aws:logs:*:*:*" + ] + } + ], + "version": "2012-10-17" + } + } + }, + "resource": { + "aws_cloudwatch_log_group": { + "test-versioned-lambda_log-group_CB8A67E7": { + "depends_on": [ + "aws_lambda_function.test-versioned-lambda_D818669D" + ], + "name": "/aws/lambda/\${aws_lambda_function.test-versioned-lambda_D818669D.function_name}", + "retention_in_days": 14 + } + }, + "aws_iam_policy": { + "test-versioned-lambda_execution-policy_A934214F": { + "name": "Test-Lambda-ExecutionRolePolicy", + "policy": "\${data.aws_iam_policy_document.test-versioned-lambda_execution-policy-document_BA37CD4D.json}" + } + }, + "aws_iam_role": { + "test-versioned-lambda_execution-role_068103E3": { + "assume_role_policy": "\${data.aws_iam_policy_document.test-versioned-lambda_assume-policy-document_54E2E6E3.json}", + "name": "Test-Lambda-ExecutionRole" + } + }, + "aws_iam_role_policy_attachment": { + "test-versioned-lambda_execution-role-policy-attachment_30E5BCA4": { + "depends_on": [ + "aws_iam_role.test-versioned-lambda_execution-role_068103E3", + "aws_iam_policy.test-versioned-lambda_execution-policy_A934214F" + ], + "policy_arn": "\${aws_iam_policy.test-versioned-lambda_execution-policy_A934214F.arn}", + "role": "\${aws_iam_role.test-versioned-lambda_execution-role_068103E3.name}" + } + }, + "aws_lambda_alias": { + "test-versioned-lambda_alias_60AF9CE1": { + "depends_on": [ + "aws_lambda_function.test-versioned-lambda_D818669D" + ], + "function_name": "\${aws_lambda_function.test-versioned-lambda_D818669D.function_name}", + "function_version": "\${element(split(\\":\\", aws_lambda_function.test-versioned-lambda_D818669D.qualified_arn), 7)}", + "lifecycle": { + "ignore_changes": [ + "function_version" + ] + }, + "name": "DEPLOYED" + } + }, + "aws_lambda_function": { + "test-versioned-lambda_D818669D": { + "filename": "\${data.archive_file.test-versioned-lambda_lambda-default-file_E2490368.output_path}", + "function_name": "Test-Lambda-Function", + "handler": "index.handler", + "lifecycle": { + "ignore_changes": [ + "filename", + "source_code_hash" + ] + }, + "memory_size": 128, + "publish": true, + "reserved_concurrent_executions": -1, + "role": "\${aws_iam_role.test-versioned-lambda_execution-role_068103E3.arn}", + "runtime": "nodejs14.x", + "source_code_hash": "\${data.archive_file.test-versioned-lambda_lambda-default-file_E2490368.output_base64sha256}", + "timeout": 5 + } + }, + "aws_s3_bucket": { + "test-versioned-lambda_code-bucket_C658EE92": { + "bucket": "test-bucket", + "force_destroy": true + } + }, + "aws_s3_bucket_acl": { + "test-versioned-lambda_code-bucket-acl_3407F3AC": { + "acl": "private", + "bucket": "\${aws_s3_bucket.test-versioned-lambda_code-bucket_C658EE92.id}", + "depends_on": [ + "aws_s3_bucket_ownership_controls.test-versioned-lambda_code-bucket-ownership-controls_7D2C8F19" + ] + } + }, + "aws_s3_bucket_ownership_controls": { + "test-versioned-lambda_code-bucket-ownership-controls_7D2C8F19": { + "bucket": "\${aws_s3_bucket.test-versioned-lambda_code-bucket_C658EE92.id}", + "rule": { + "object_ownership": "BucketOwnerPreferred" + } + } + }, + "aws_s3_bucket_public_access_block": { + "test-versioned-lambda_code-bucket-public-access-block_9662548B": { + "block_public_acls": true, + "block_public_policy": true, + "bucket": "\${aws_s3_bucket.test-versioned-lambda_code-bucket_C658EE92.id}" + } + } + } +}" +`; + +exports[`ApplicationVersionedLambda renders a versioned lambda 1`] = ` +"{ + "data": { + "archive_file": { + "test-versioned-lambda_lambda-default-file_E2490368": { + "output_path": "index.py.zip", + "source": [ + { + "content": "import json\\ndef handler(event, context):\\n\\t print(event)\\n\\t return {'statusCode': 200, 'headers': {'dance': 'party'}, 'body': json.dumps({'electric': 'boogaloo'}), 'isBase64Encoded': False}", + "filename": "index.py" + } + ], + "type": "zip" + } + }, + "aws_iam_policy_document": { + "test-versioned-lambda_assume-policy-document_54E2E6E3": { + "statement": [ + { + "actions": [ + "sts:AssumeRole" + ], + "effect": "Allow", + "principals": [ + { + "identifiers": [ + "lambda.amazonaws.com", + "edgelambda.amazonaws.com" + ], + "type": "Service" + } + ] + } + ], + "version": "2012-10-17" + }, + "test-versioned-lambda_execution-policy-document_BA37CD4D": { + "statement": [ + { + "actions": [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents", + "logs:DescribeLogStreams" + ], + "effect": "Allow", + "resources": [ + "arn:aws:logs:*:*:*" + ] + } + ], + "version": "2012-10-17" + } + } + }, + "resource": { + "aws_cloudwatch_log_group": { + "test-versioned-lambda_log-group_CB8A67E7": { + "depends_on": [ + "aws_lambda_function.test-versioned-lambda_D818669D" + ], + "name": "/aws/lambda/\${aws_lambda_function.test-versioned-lambda_D818669D.function_name}", + "retention_in_days": 14 + } + }, + "aws_iam_policy": { + "test-versioned-lambda_execution-policy_A934214F": { + "name": "Test-Lambda-ExecutionRolePolicy", + "policy": "\${data.aws_iam_policy_document.test-versioned-lambda_execution-policy-document_BA37CD4D.json}" + } + }, + "aws_iam_role": { + "test-versioned-lambda_execution-role_068103E3": { + "assume_role_policy": "\${data.aws_iam_policy_document.test-versioned-lambda_assume-policy-document_54E2E6E3.json}", + "name": "Test-Lambda-ExecutionRole" + } + }, + "aws_iam_role_policy_attachment": { + "test-versioned-lambda_execution-role-policy-attachment_30E5BCA4": { + "depends_on": [ + "aws_iam_role.test-versioned-lambda_execution-role_068103E3", + "aws_iam_policy.test-versioned-lambda_execution-policy_A934214F" + ], + "policy_arn": "\${aws_iam_policy.test-versioned-lambda_execution-policy_A934214F.arn}", + "role": "\${aws_iam_role.test-versioned-lambda_execution-role_068103E3.name}" + } + }, + "aws_lambda_alias": { + "test-versioned-lambda_alias_60AF9CE1": { + "depends_on": [ + "aws_lambda_function.test-versioned-lambda_D818669D" + ], + "function_name": "\${aws_lambda_function.test-versioned-lambda_D818669D.function_name}", + "function_version": "\${element(split(\\":\\", aws_lambda_function.test-versioned-lambda_D818669D.qualified_arn), 7)}", + "lifecycle": { + "ignore_changes": [ + "function_version" + ] + }, + "name": "DEPLOYED" + } + }, + "aws_lambda_function": { + "test-versioned-lambda_D818669D": { + "filename": "\${data.archive_file.test-versioned-lambda_lambda-default-file_E2490368.output_path}", + "function_name": "Test-Lambda-Function", + "handler": "index.handler", + "lifecycle": { + "ignore_changes": [ + "filename", + "source_code_hash" + ] + }, + "memory_size": 128, + "publish": true, + "reserved_concurrent_executions": -1, + "role": "\${aws_iam_role.test-versioned-lambda_execution-role_068103E3.arn}", + "runtime": "python3.8", + "source_code_hash": "\${data.archive_file.test-versioned-lambda_lambda-default-file_E2490368.output_base64sha256}", + "timeout": 5 + } + }, + "aws_s3_bucket": { + "test-versioned-lambda_code-bucket_C658EE92": { + "bucket": "test-bucket", + "force_destroy": true + } + }, + "aws_s3_bucket_acl": { + "test-versioned-lambda_code-bucket-acl_3407F3AC": { + "acl": "private", + "bucket": "\${aws_s3_bucket.test-versioned-lambda_code-bucket_C658EE92.id}", + "depends_on": [ + "aws_s3_bucket_ownership_controls.test-versioned-lambda_code-bucket-ownership-controls_7D2C8F19" + ] + } + }, + "aws_s3_bucket_ownership_controls": { + "test-versioned-lambda_code-bucket-ownership-controls_7D2C8F19": { + "bucket": "\${aws_s3_bucket.test-versioned-lambda_code-bucket_C658EE92.id}", + "rule": { + "object_ownership": "BucketOwnerPreferred" + } + } + }, + "aws_s3_bucket_public_access_block": { + "test-versioned-lambda_code-bucket-public-access-block_9662548B": { + "block_public_acls": true, + "block_public_policy": true, + "bucket": "\${aws_s3_bucket.test-versioned-lambda_code-bucket_C658EE92.id}" + } + } + } +}" +`; + +exports[`ApplicationVersionedLambda renders a versioned lambda with description 1`] = ` +"{ + "data": { + "archive_file": { + "test-versioned-lambda_lambda-default-file_E2490368": { + "output_path": "index.py.zip", + "source": [ + { + "content": "import json\\ndef handler(event, context):\\n\\t print(event)\\n\\t return {'statusCode': 200, 'headers': {'dance': 'party'}, 'body': json.dumps({'electric': 'boogaloo'}), 'isBase64Encoded': False}", + "filename": "index.py" + } + ], + "type": "zip" + } + }, + "aws_iam_policy_document": { + "test-versioned-lambda_assume-policy-document_54E2E6E3": { + "statement": [ + { + "actions": [ + "sts:AssumeRole" + ], + "effect": "Allow", + "principals": [ + { + "identifiers": [ + "lambda.amazonaws.com", + "edgelambda.amazonaws.com" + ], + "type": "Service" + } + ] + } + ], + "version": "2012-10-17" + }, + "test-versioned-lambda_execution-policy-document_BA37CD4D": { + "statement": [ + { + "actions": [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents", + "logs:DescribeLogStreams" + ], + "effect": "Allow", + "resources": [ + "arn:aws:logs:*:*:*" + ] + } + ], + "version": "2012-10-17" + } + } + }, + "resource": { + "aws_cloudwatch_log_group": { + "test-versioned-lambda_log-group_CB8A67E7": { + "depends_on": [ + "aws_lambda_function.test-versioned-lambda_D818669D" + ], + "name": "/aws/lambda/\${aws_lambda_function.test-versioned-lambda_D818669D.function_name}", + "retention_in_days": 14 + } + }, + "aws_iam_policy": { + "test-versioned-lambda_execution-policy_A934214F": { + "name": "Test-Lambda-ExecutionRolePolicy", + "policy": "\${data.aws_iam_policy_document.test-versioned-lambda_execution-policy-document_BA37CD4D.json}" + } + }, + "aws_iam_role": { + "test-versioned-lambda_execution-role_068103E3": { + "assume_role_policy": "\${data.aws_iam_policy_document.test-versioned-lambda_assume-policy-document_54E2E6E3.json}", + "name": "Test-Lambda-ExecutionRole" + } + }, + "aws_iam_role_policy_attachment": { + "test-versioned-lambda_execution-role-policy-attachment_30E5BCA4": { + "depends_on": [ + "aws_iam_role.test-versioned-lambda_execution-role_068103E3", + "aws_iam_policy.test-versioned-lambda_execution-policy_A934214F" + ], + "policy_arn": "\${aws_iam_policy.test-versioned-lambda_execution-policy_A934214F.arn}", + "role": "\${aws_iam_role.test-versioned-lambda_execution-role_068103E3.name}" + } + }, + "aws_lambda_alias": { + "test-versioned-lambda_alias_60AF9CE1": { + "depends_on": [ + "aws_lambda_function.test-versioned-lambda_D818669D" + ], + "function_name": "\${aws_lambda_function.test-versioned-lambda_D818669D.function_name}", + "function_version": "\${element(split(\\":\\", aws_lambda_function.test-versioned-lambda_D818669D.qualified_arn), 7)}", + "lifecycle": { + "ignore_changes": [ + "function_version" + ] + }, + "name": "DEPLOYED" + } + }, + "aws_lambda_function": { + "test-versioned-lambda_D818669D": { + "filename": "\${data.archive_file.test-versioned-lambda_lambda-default-file_E2490368.output_path}", + "function_name": "Test-Lambda-Function", + "handler": "index.handler", + "lifecycle": { + "ignore_changes": [ + "filename", + "source_code_hash" + ] + }, + "memory_size": 128, + "publish": true, + "reserved_concurrent_executions": -1, + "role": "\${aws_iam_role.test-versioned-lambda_execution-role_068103E3.arn}", + "runtime": "python3.8", + "source_code_hash": "\${data.archive_file.test-versioned-lambda_lambda-default-file_E2490368.output_base64sha256}", + "timeout": 5 + } + }, + "aws_s3_bucket": { + "test-versioned-lambda_code-bucket_C658EE92": { + "bucket": "test-bucket", + "force_destroy": true + } + }, + "aws_s3_bucket_acl": { + "test-versioned-lambda_code-bucket-acl_3407F3AC": { + "acl": "private", + "bucket": "\${aws_s3_bucket.test-versioned-lambda_code-bucket_C658EE92.id}", + "depends_on": [ + "aws_s3_bucket_ownership_controls.test-versioned-lambda_code-bucket-ownership-controls_7D2C8F19" + ] + } + }, + "aws_s3_bucket_ownership_controls": { + "test-versioned-lambda_code-bucket-ownership-controls_7D2C8F19": { + "bucket": "\${aws_s3_bucket.test-versioned-lambda_code-bucket_C658EE92.id}", + "rule": { + "object_ownership": "BucketOwnerPreferred" + } + } + }, + "aws_s3_bucket_public_access_block": { + "test-versioned-lambda_code-bucket-public-access-block_9662548B": { + "block_public_acls": true, + "block_public_policy": true, + "bucket": "\${aws_s3_bucket.test-versioned-lambda_code-bucket_C658EE92.id}" + } + } + } +}" +`; + +exports[`ApplicationVersionedLambda renders a versioned lambda with environment variables 1`] = ` +"{ + "data": { + "archive_file": { + "test-versioned-lambda_lambda-default-file_E2490368": { + "output_path": "index.py.zip", + "source": [ + { + "content": "import json\\ndef handler(event, context):\\n\\t print(event)\\n\\t return {'statusCode': 200, 'headers': {'dance': 'party'}, 'body': json.dumps({'electric': 'boogaloo'}), 'isBase64Encoded': False}", + "filename": "index.py" + } + ], + "type": "zip" + } + }, + "aws_iam_policy_document": { + "test-versioned-lambda_assume-policy-document_54E2E6E3": { + "statement": [ + { + "actions": [ + "sts:AssumeRole" + ], + "effect": "Allow", + "principals": [ + { + "identifiers": [ + "lambda.amazonaws.com", + "edgelambda.amazonaws.com" + ], + "type": "Service" + } + ] + } + ], + "version": "2012-10-17" + }, + "test-versioned-lambda_execution-policy-document_BA37CD4D": { + "statement": [ + { + "actions": [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents", + "logs:DescribeLogStreams" + ], + "effect": "Allow", + "resources": [ + "arn:aws:logs:*:*:*" + ] + } + ], + "version": "2012-10-17" + } + } + }, + "resource": { + "aws_cloudwatch_log_group": { + "test-versioned-lambda_log-group_CB8A67E7": { + "depends_on": [ + "aws_lambda_function.test-versioned-lambda_D818669D" + ], + "name": "/aws/lambda/\${aws_lambda_function.test-versioned-lambda_D818669D.function_name}", + "retention_in_days": 14 + } + }, + "aws_iam_policy": { + "test-versioned-lambda_execution-policy_A934214F": { + "name": "Test-Lambda-ExecutionRolePolicy", + "policy": "\${data.aws_iam_policy_document.test-versioned-lambda_execution-policy-document_BA37CD4D.json}" + } + }, + "aws_iam_role": { + "test-versioned-lambda_execution-role_068103E3": { + "assume_role_policy": "\${data.aws_iam_policy_document.test-versioned-lambda_assume-policy-document_54E2E6E3.json}", + "name": "Test-Lambda-ExecutionRole" + } + }, + "aws_iam_role_policy_attachment": { + "test-versioned-lambda_execution-role-policy-attachment_30E5BCA4": { + "depends_on": [ + "aws_iam_role.test-versioned-lambda_execution-role_068103E3", + "aws_iam_policy.test-versioned-lambda_execution-policy_A934214F" + ], + "policy_arn": "\${aws_iam_policy.test-versioned-lambda_execution-policy_A934214F.arn}", + "role": "\${aws_iam_role.test-versioned-lambda_execution-role_068103E3.name}" + } + }, + "aws_lambda_alias": { + "test-versioned-lambda_alias_60AF9CE1": { + "depends_on": [ + "aws_lambda_function.test-versioned-lambda_D818669D" + ], + "function_name": "\${aws_lambda_function.test-versioned-lambda_D818669D.function_name}", + "function_version": "\${element(split(\\":\\", aws_lambda_function.test-versioned-lambda_D818669D.qualified_arn), 7)}", + "lifecycle": { + "ignore_changes": [ + "function_version" + ] + }, + "name": "DEPLOYED" + } + }, + "aws_lambda_function": { + "test-versioned-lambda_D818669D": { + "environment": { + "variables": { + "MY": "env_var", + "for": "test" + } + }, + "filename": "\${data.archive_file.test-versioned-lambda_lambda-default-file_E2490368.output_path}", + "function_name": "Test-Lambda-Function", + "handler": "index.handler", + "lifecycle": { + "ignore_changes": [ + "filename", + "source_code_hash" + ] + }, + "memory_size": 128, + "publish": true, + "reserved_concurrent_executions": -1, + "role": "\${aws_iam_role.test-versioned-lambda_execution-role_068103E3.arn}", + "runtime": "python3.8", + "source_code_hash": "\${data.archive_file.test-versioned-lambda_lambda-default-file_E2490368.output_base64sha256}", + "timeout": 5 + } + }, + "aws_s3_bucket": { + "test-versioned-lambda_code-bucket_C658EE92": { + "bucket": "test-bucket", + "force_destroy": true + } + }, + "aws_s3_bucket_acl": { + "test-versioned-lambda_code-bucket-acl_3407F3AC": { + "acl": "private", + "bucket": "\${aws_s3_bucket.test-versioned-lambda_code-bucket_C658EE92.id}", + "depends_on": [ + "aws_s3_bucket_ownership_controls.test-versioned-lambda_code-bucket-ownership-controls_7D2C8F19" + ] + } + }, + "aws_s3_bucket_ownership_controls": { + "test-versioned-lambda_code-bucket-ownership-controls_7D2C8F19": { + "bucket": "\${aws_s3_bucket.test-versioned-lambda_code-bucket_C658EE92.id}", + "rule": { + "object_ownership": "BucketOwnerPreferred" + } + } + }, + "aws_s3_bucket_public_access_block": { + "test-versioned-lambda_code-bucket-public-access-block_9662548B": { + "block_public_acls": true, + "block_public_policy": true, + "bucket": "\${aws_s3_bucket.test-versioned-lambda_code-bucket_C658EE92.id}" + } + } + } +}" +`; + +exports[`ApplicationVersionedLambda renders a versioned lambda with execution policy statements 1`] = ` +"{ + "data": { + "archive_file": { + "test-versioned-lambda_lambda-default-file_E2490368": { + "output_path": "index.py.zip", + "source": [ + { + "content": "import json\\ndef handler(event, context):\\n\\t print(event)\\n\\t return {'statusCode': 200, 'headers': {'dance': 'party'}, 'body': json.dumps({'electric': 'boogaloo'}), 'isBase64Encoded': False}", + "filename": "index.py" + } + ], + "type": "zip" + } + }, + "aws_iam_policy_document": { + "test-versioned-lambda_assume-policy-document_54E2E6E3": { + "statement": [ + { + "actions": [ + "sts:AssumeRole" + ], + "effect": "Allow", + "principals": [ + { + "identifiers": [ + "lambda.amazonaws.com", + "edgelambda.amazonaws.com" + ], + "type": "Service" + } + ] + } + ], + "version": "2012-10-17" + }, + "test-versioned-lambda_execution-policy-document_BA37CD4D": { + "statement": [ + { + "actions": [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents", + "logs:DescribeLogStreams" + ], + "effect": "Allow", + "resources": [ + "arn:aws:logs:*:*:*" + ] + }, + { + "actions": [ + "*" + ], + "effect": "Allow", + "resources": [ + "*" + ] + } + ], + "version": "2012-10-17" + } + } + }, + "resource": { + "aws_cloudwatch_log_group": { + "test-versioned-lambda_log-group_CB8A67E7": { + "depends_on": [ + "aws_lambda_function.test-versioned-lambda_D818669D" + ], + "name": "/aws/lambda/\${aws_lambda_function.test-versioned-lambda_D818669D.function_name}", + "retention_in_days": 14 + } + }, + "aws_iam_policy": { + "test-versioned-lambda_execution-policy_A934214F": { + "name": "Test-Lambda-ExecutionRolePolicy", + "policy": "\${data.aws_iam_policy_document.test-versioned-lambda_execution-policy-document_BA37CD4D.json}" + } + }, + "aws_iam_role": { + "test-versioned-lambda_execution-role_068103E3": { + "assume_role_policy": "\${data.aws_iam_policy_document.test-versioned-lambda_assume-policy-document_54E2E6E3.json}", + "name": "Test-Lambda-ExecutionRole" + } + }, + "aws_iam_role_policy_attachment": { + "test-versioned-lambda_execution-role-policy-attachment_30E5BCA4": { + "depends_on": [ + "aws_iam_role.test-versioned-lambda_execution-role_068103E3", + "aws_iam_policy.test-versioned-lambda_execution-policy_A934214F" + ], + "policy_arn": "\${aws_iam_policy.test-versioned-lambda_execution-policy_A934214F.arn}", + "role": "\${aws_iam_role.test-versioned-lambda_execution-role_068103E3.name}" + } + }, + "aws_lambda_alias": { + "test-versioned-lambda_alias_60AF9CE1": { + "depends_on": [ + "aws_lambda_function.test-versioned-lambda_D818669D" + ], + "function_name": "\${aws_lambda_function.test-versioned-lambda_D818669D.function_name}", + "function_version": "\${element(split(\\":\\", aws_lambda_function.test-versioned-lambda_D818669D.qualified_arn), 7)}", + "lifecycle": { + "ignore_changes": [ + "function_version" + ] + }, + "name": "DEPLOYED" + } + }, + "aws_lambda_function": { + "test-versioned-lambda_D818669D": { + "filename": "\${data.archive_file.test-versioned-lambda_lambda-default-file_E2490368.output_path}", + "function_name": "Test-Lambda-Function", + "handler": "index.handler", + "lifecycle": { + "ignore_changes": [ + "filename", + "source_code_hash" + ] + }, + "memory_size": 128, + "publish": true, + "reserved_concurrent_executions": -1, + "role": "\${aws_iam_role.test-versioned-lambda_execution-role_068103E3.arn}", + "runtime": "python3.8", + "source_code_hash": "\${data.archive_file.test-versioned-lambda_lambda-default-file_E2490368.output_base64sha256}", + "timeout": 5 + } + }, + "aws_s3_bucket": { + "test-versioned-lambda_code-bucket_C658EE92": { + "bucket": "test-bucket", + "force_destroy": true + } + }, + "aws_s3_bucket_acl": { + "test-versioned-lambda_code-bucket-acl_3407F3AC": { + "acl": "private", + "bucket": "\${aws_s3_bucket.test-versioned-lambda_code-bucket_C658EE92.id}", + "depends_on": [ + "aws_s3_bucket_ownership_controls.test-versioned-lambda_code-bucket-ownership-controls_7D2C8F19" + ] + } + }, + "aws_s3_bucket_ownership_controls": { + "test-versioned-lambda_code-bucket-ownership-controls_7D2C8F19": { + "bucket": "\${aws_s3_bucket.test-versioned-lambda_code-bucket_C658EE92.id}", + "rule": { + "object_ownership": "BucketOwnerPreferred" + } + } + }, + "aws_s3_bucket_public_access_block": { + "test-versioned-lambda_code-bucket-public-access-block_9662548B": { + "block_public_acls": true, + "block_public_policy": true, + "bucket": "\${aws_s3_bucket.test-versioned-lambda_code-bucket_C658EE92.id}" + } + } + } +}" +`; + +exports[`ApplicationVersionedLambda renders a versioned lambda with log retention 1`] = ` +"{ + "data": { + "archive_file": { + "test-versioned-lambda_lambda-default-file_E2490368": { + "output_path": "index.py.zip", + "source": [ + { + "content": "import json\\ndef handler(event, context):\\n\\t print(event)\\n\\t return {'statusCode': 200, 'headers': {'dance': 'party'}, 'body': json.dumps({'electric': 'boogaloo'}), 'isBase64Encoded': False}", + "filename": "index.py" + } + ], + "type": "zip" + } + }, + "aws_iam_policy_document": { + "test-versioned-lambda_assume-policy-document_54E2E6E3": { + "statement": [ + { + "actions": [ + "sts:AssumeRole" + ], + "effect": "Allow", + "principals": [ + { + "identifiers": [ + "lambda.amazonaws.com", + "edgelambda.amazonaws.com" + ], + "type": "Service" + } + ] + } + ], + "version": "2012-10-17" + }, + "test-versioned-lambda_execution-policy-document_BA37CD4D": { + "statement": [ + { + "actions": [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents", + "logs:DescribeLogStreams" + ], + "effect": "Allow", + "resources": [ + "arn:aws:logs:*:*:*" + ] + } + ], + "version": "2012-10-17" + } + } + }, + "resource": { + "aws_cloudwatch_log_group": { + "test-versioned-lambda_log-group_CB8A67E7": { + "depends_on": [ + "aws_lambda_function.test-versioned-lambda_D818669D" + ], + "name": "/aws/lambda/\${aws_lambda_function.test-versioned-lambda_D818669D.function_name}", + "retention_in_days": 10 + } + }, + "aws_iam_policy": { + "test-versioned-lambda_execution-policy_A934214F": { + "name": "Test-Lambda-ExecutionRolePolicy", + "policy": "\${data.aws_iam_policy_document.test-versioned-lambda_execution-policy-document_BA37CD4D.json}" + } + }, + "aws_iam_role": { + "test-versioned-lambda_execution-role_068103E3": { + "assume_role_policy": "\${data.aws_iam_policy_document.test-versioned-lambda_assume-policy-document_54E2E6E3.json}", + "name": "Test-Lambda-ExecutionRole" + } + }, + "aws_iam_role_policy_attachment": { + "test-versioned-lambda_execution-role-policy-attachment_30E5BCA4": { + "depends_on": [ + "aws_iam_role.test-versioned-lambda_execution-role_068103E3", + "aws_iam_policy.test-versioned-lambda_execution-policy_A934214F" + ], + "policy_arn": "\${aws_iam_policy.test-versioned-lambda_execution-policy_A934214F.arn}", + "role": "\${aws_iam_role.test-versioned-lambda_execution-role_068103E3.name}" + } + }, + "aws_lambda_alias": { + "test-versioned-lambda_alias_60AF9CE1": { + "depends_on": [ + "aws_lambda_function.test-versioned-lambda_D818669D" + ], + "function_name": "\${aws_lambda_function.test-versioned-lambda_D818669D.function_name}", + "function_version": "\${element(split(\\":\\", aws_lambda_function.test-versioned-lambda_D818669D.qualified_arn), 7)}", + "lifecycle": { + "ignore_changes": [ + "function_version" + ] + }, + "name": "DEPLOYED" + } + }, + "aws_lambda_function": { + "test-versioned-lambda_D818669D": { + "filename": "\${data.archive_file.test-versioned-lambda_lambda-default-file_E2490368.output_path}", + "function_name": "Test-Lambda-Function", + "handler": "index.handler", + "lifecycle": { + "ignore_changes": [ + "filename", + "source_code_hash" + ] + }, + "memory_size": 128, + "publish": true, + "reserved_concurrent_executions": -1, + "role": "\${aws_iam_role.test-versioned-lambda_execution-role_068103E3.arn}", + "runtime": "python3.8", + "source_code_hash": "\${data.archive_file.test-versioned-lambda_lambda-default-file_E2490368.output_base64sha256}", + "timeout": 5 + } + }, + "aws_s3_bucket": { + "test-versioned-lambda_code-bucket_C658EE92": { + "bucket": "test-bucket", + "force_destroy": true + } + }, + "aws_s3_bucket_acl": { + "test-versioned-lambda_code-bucket-acl_3407F3AC": { + "acl": "private", + "bucket": "\${aws_s3_bucket.test-versioned-lambda_code-bucket_C658EE92.id}", + "depends_on": [ + "aws_s3_bucket_ownership_controls.test-versioned-lambda_code-bucket-ownership-controls_7D2C8F19" + ] + } + }, + "aws_s3_bucket_ownership_controls": { + "test-versioned-lambda_code-bucket-ownership-controls_7D2C8F19": { + "bucket": "\${aws_s3_bucket.test-versioned-lambda_code-bucket_C658EE92.id}", + "rule": { + "object_ownership": "BucketOwnerPreferred" + } + } + }, + "aws_s3_bucket_public_access_block": { + "test-versioned-lambda_code-bucket-public-access-block_9662548B": { + "block_public_acls": true, + "block_public_policy": true, + "bucket": "\${aws_s3_bucket.test-versioned-lambda_code-bucket_C658EE92.id}" + } + } + } +}" +`; + +exports[`ApplicationVersionedLambda renders a versioned lambda with publish ignored 1`] = ` +"{ + "data": { + "archive_file": { + "test-versioned-lambda_lambda-default-file_E2490368": { + "output_path": "index.py.zip", + "source": [ + { + "content": "import json\\ndef handler(event, context):\\n\\t print(event)\\n\\t return {'statusCode': 200, 'headers': {'dance': 'party'}, 'body': json.dumps({'electric': 'boogaloo'}), 'isBase64Encoded': False}", + "filename": "index.py" + } + ], + "type": "zip" + } + }, + "aws_iam_policy_document": { + "test-versioned-lambda_assume-policy-document_54E2E6E3": { + "statement": [ + { + "actions": [ + "sts:AssumeRole" + ], + "effect": "Allow", + "principals": [ + { + "identifiers": [ + "lambda.amazonaws.com", + "edgelambda.amazonaws.com" + ], + "type": "Service" + } + ] + } + ], + "version": "2012-10-17" + }, + "test-versioned-lambda_execution-policy-document_BA37CD4D": { + "statement": [ + { + "actions": [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents", + "logs:DescribeLogStreams" + ], + "effect": "Allow", + "resources": [ + "arn:aws:logs:*:*:*" + ] + } + ], + "version": "2012-10-17" + } + } + }, + "resource": { + "aws_cloudwatch_log_group": { + "test-versioned-lambda_log-group_CB8A67E7": { + "depends_on": [ + "aws_lambda_function.test-versioned-lambda_D818669D" + ], + "name": "/aws/lambda/\${aws_lambda_function.test-versioned-lambda_D818669D.function_name}", + "retention_in_days": 14 + } + }, + "aws_iam_policy": { + "test-versioned-lambda_execution-policy_A934214F": { + "name": "Test-Lambda-ExecutionRolePolicy", + "policy": "\${data.aws_iam_policy_document.test-versioned-lambda_execution-policy-document_BA37CD4D.json}" + } + }, + "aws_iam_role": { + "test-versioned-lambda_execution-role_068103E3": { + "assume_role_policy": "\${data.aws_iam_policy_document.test-versioned-lambda_assume-policy-document_54E2E6E3.json}", + "name": "Test-Lambda-ExecutionRole" + } + }, + "aws_iam_role_policy_attachment": { + "test-versioned-lambda_execution-role-policy-attachment_30E5BCA4": { + "depends_on": [ + "aws_iam_role.test-versioned-lambda_execution-role_068103E3", + "aws_iam_policy.test-versioned-lambda_execution-policy_A934214F" + ], + "policy_arn": "\${aws_iam_policy.test-versioned-lambda_execution-policy_A934214F.arn}", + "role": "\${aws_iam_role.test-versioned-lambda_execution-role_068103E3.name}" + } + }, + "aws_lambda_alias": { + "test-versioned-lambda_alias_60AF9CE1": { + "depends_on": [ + "aws_lambda_function.test-versioned-lambda_D818669D" + ], + "function_name": "\${aws_lambda_function.test-versioned-lambda_D818669D.function_name}", + "function_version": "\${element(split(\\":\\", aws_lambda_function.test-versioned-lambda_D818669D.qualified_arn), 7)}", + "lifecycle": { + "ignore_changes": [ + "function_version" + ] + }, + "name": "DEPLOYED" + } + }, + "aws_lambda_function": { + "test-versioned-lambda_D818669D": { + "filename": "\${data.archive_file.test-versioned-lambda_lambda-default-file_E2490368.output_path}", + "function_name": "Test-Lambda-Function", + "handler": "index.handler", + "lifecycle": { + "ignore_changes": [ + "filename", + "source_code_hash", + "publish" + ] + }, + "memory_size": 128, + "publish": true, + "reserved_concurrent_executions": -1, + "role": "\${aws_iam_role.test-versioned-lambda_execution-role_068103E3.arn}", + "runtime": "python3.8", + "source_code_hash": "\${data.archive_file.test-versioned-lambda_lambda-default-file_E2490368.output_base64sha256}", + "timeout": 5 + } + }, + "aws_s3_bucket": { + "test-versioned-lambda_code-bucket_C658EE92": { + "bucket": "test-bucket", + "force_destroy": true + } + }, + "aws_s3_bucket_acl": { + "test-versioned-lambda_code-bucket-acl_3407F3AC": { + "acl": "private", + "bucket": "\${aws_s3_bucket.test-versioned-lambda_code-bucket_C658EE92.id}", + "depends_on": [ + "aws_s3_bucket_ownership_controls.test-versioned-lambda_code-bucket-ownership-controls_7D2C8F19" + ] + } + }, + "aws_s3_bucket_ownership_controls": { + "test-versioned-lambda_code-bucket-ownership-controls_7D2C8F19": { + "bucket": "\${aws_s3_bucket.test-versioned-lambda_code-bucket_C658EE92.id}", + "rule": { + "object_ownership": "BucketOwnerPreferred" + } + } + }, + "aws_s3_bucket_public_access_block": { + "test-versioned-lambda_code-bucket-public-access-block_9662548B": { + "block_public_acls": true, + "block_public_policy": true, + "bucket": "\${aws_s3_bucket.test-versioned-lambda_code-bucket_C658EE92.id}" + } + } + } +}" +`; + +exports[`ApplicationVersionedLambda renders a versioned lambda with s3 bucket 1`] = ` +"{ + "data": { + "archive_file": { + "test-versioned-lambda_lambda-default-file_E2490368": { + "output_path": "index.py.zip", + "source": [ + { + "content": "import json\\ndef handler(event, context):\\n\\t print(event)\\n\\t return {'statusCode': 200, 'headers': {'dance': 'party'}, 'body': json.dumps({'electric': 'boogaloo'}), 'isBase64Encoded': False}", + "filename": "index.py" + } + ], + "type": "zip" + } + }, + "aws_iam_policy_document": { + "test-versioned-lambda_assume-policy-document_54E2E6E3": { + "statement": [ + { + "actions": [ + "sts:AssumeRole" + ], + "effect": "Allow", + "principals": [ + { + "identifiers": [ + "lambda.amazonaws.com", + "edgelambda.amazonaws.com" + ], + "type": "Service" + } + ] + } + ], + "version": "2012-10-17" + }, + "test-versioned-lambda_execution-policy-document_BA37CD4D": { + "statement": [ + { + "actions": [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents", + "logs:DescribeLogStreams" + ], + "effect": "Allow", + "resources": [ + "arn:aws:logs:*:*:*" + ] + } + ], + "version": "2012-10-17" + } + } + }, + "resource": { + "aws_cloudwatch_log_group": { + "test-versioned-lambda_log-group_CB8A67E7": { + "depends_on": [ + "aws_lambda_function.test-versioned-lambda_D818669D" + ], + "name": "/aws/lambda/\${aws_lambda_function.test-versioned-lambda_D818669D.function_name}", + "retention_in_days": 14 + } + }, + "aws_iam_policy": { + "test-versioned-lambda_execution-policy_A934214F": { + "name": "Test-Lambda-ExecutionRolePolicy", + "policy": "\${data.aws_iam_policy_document.test-versioned-lambda_execution-policy-document_BA37CD4D.json}" + } + }, + "aws_iam_role": { + "test-versioned-lambda_execution-role_068103E3": { + "assume_role_policy": "\${data.aws_iam_policy_document.test-versioned-lambda_assume-policy-document_54E2E6E3.json}", + "name": "Test-Lambda-ExecutionRole" + } + }, + "aws_iam_role_policy_attachment": { + "test-versioned-lambda_execution-role-policy-attachment_30E5BCA4": { + "depends_on": [ + "aws_iam_role.test-versioned-lambda_execution-role_068103E3", + "aws_iam_policy.test-versioned-lambda_execution-policy_A934214F" + ], + "policy_arn": "\${aws_iam_policy.test-versioned-lambda_execution-policy_A934214F.arn}", + "role": "\${aws_iam_role.test-versioned-lambda_execution-role_068103E3.name}" + } + }, + "aws_lambda_alias": { + "test-versioned-lambda_alias_60AF9CE1": { + "depends_on": [ + "aws_lambda_function.test-versioned-lambda_D818669D" + ], + "function_name": "\${aws_lambda_function.test-versioned-lambda_D818669D.function_name}", + "function_version": "\${element(split(\\":\\", aws_lambda_function.test-versioned-lambda_D818669D.qualified_arn), 7)}", + "lifecycle": { + "ignore_changes": [ + "function_version" + ] + }, + "name": "DEPLOYED" + } + }, + "aws_lambda_function": { + "test-versioned-lambda_D818669D": { + "filename": "\${data.archive_file.test-versioned-lambda_lambda-default-file_E2490368.output_path}", + "function_name": "Test-Lambda-Function", + "handler": "index.handler", + "lifecycle": { + "ignore_changes": [ + "filename", + "source_code_hash" + ] + }, + "memory_size": 128, + "publish": true, + "reserved_concurrent_executions": -1, + "role": "\${aws_iam_role.test-versioned-lambda_execution-role_068103E3.arn}", + "runtime": "python3.8", + "source_code_hash": "\${data.archive_file.test-versioned-lambda_lambda-default-file_E2490368.output_base64sha256}", + "timeout": 5 + } + }, + "aws_s3_bucket": { + "test-versioned-lambda_code-bucket_C658EE92": { + "bucket": "test-bucket", + "force_destroy": true + } + }, + "aws_s3_bucket_acl": { + "test-versioned-lambda_code-bucket-acl_3407F3AC": { + "acl": "private", + "bucket": "\${aws_s3_bucket.test-versioned-lambda_code-bucket_C658EE92.id}", + "depends_on": [ + "aws_s3_bucket_ownership_controls.test-versioned-lambda_code-bucket-ownership-controls_7D2C8F19" + ] + } + }, + "aws_s3_bucket_ownership_controls": { + "test-versioned-lambda_code-bucket-ownership-controls_7D2C8F19": { + "bucket": "\${aws_s3_bucket.test-versioned-lambda_code-bucket_C658EE92.id}", + "rule": { + "object_ownership": "BucketOwnerPreferred" + } + } + }, + "aws_s3_bucket_public_access_block": { + "test-versioned-lambda_code-bucket-public-access-block_9662548B": { + "block_public_acls": true, + "block_public_policy": true, + "bucket": "\${aws_s3_bucket.test-versioned-lambda_code-bucket_C658EE92.id}" + } + } + } +}" +`; + +exports[`ApplicationVersionedLambda renders a versioned lambda with tags 1`] = ` +"{ + "data": { + "archive_file": { + "test-versioned-lambda_lambda-default-file_E2490368": { + "output_path": "index.py.zip", + "source": [ + { + "content": "import json\\ndef handler(event, context):\\n\\t print(event)\\n\\t return {'statusCode': 200, 'headers': {'dance': 'party'}, 'body': json.dumps({'electric': 'boogaloo'}), 'isBase64Encoded': False}", + "filename": "index.py" + } + ], + "type": "zip" + } + }, + "aws_iam_policy_document": { + "test-versioned-lambda_assume-policy-document_54E2E6E3": { + "statement": [ + { + "actions": [ + "sts:AssumeRole" + ], + "effect": "Allow", + "principals": [ + { + "identifiers": [ + "lambda.amazonaws.com", + "edgelambda.amazonaws.com" + ], + "type": "Service" + } + ] + } + ], + "version": "2012-10-17" + }, + "test-versioned-lambda_execution-policy-document_BA37CD4D": { + "statement": [ + { + "actions": [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents", + "logs:DescribeLogStreams" + ], + "effect": "Allow", + "resources": [ + "arn:aws:logs:*:*:*" + ] + } + ], + "version": "2012-10-17" + } + } + }, + "resource": { + "aws_cloudwatch_log_group": { + "test-versioned-lambda_log-group_CB8A67E7": { + "depends_on": [ + "aws_lambda_function.test-versioned-lambda_D818669D" + ], + "name": "/aws/lambda/\${aws_lambda_function.test-versioned-lambda_D818669D.function_name}", + "retention_in_days": 14, + "tags": { + "for": "test", + "my": "tag" + } + } + }, + "aws_iam_policy": { + "test-versioned-lambda_execution-policy_A934214F": { + "name": "Test-Lambda-ExecutionRolePolicy", + "policy": "\${data.aws_iam_policy_document.test-versioned-lambda_execution-policy-document_BA37CD4D.json}", + "tags": { + "for": "test", + "my": "tag" + } + } + }, + "aws_iam_role": { + "test-versioned-lambda_execution-role_068103E3": { + "assume_role_policy": "\${data.aws_iam_policy_document.test-versioned-lambda_assume-policy-document_54E2E6E3.json}", + "name": "Test-Lambda-ExecutionRole", + "tags": { + "for": "test", + "my": "tag" + } + } + }, + "aws_iam_role_policy_attachment": { + "test-versioned-lambda_execution-role-policy-attachment_30E5BCA4": { + "depends_on": [ + "aws_iam_role.test-versioned-lambda_execution-role_068103E3", + "aws_iam_policy.test-versioned-lambda_execution-policy_A934214F" + ], + "policy_arn": "\${aws_iam_policy.test-versioned-lambda_execution-policy_A934214F.arn}", + "role": "\${aws_iam_role.test-versioned-lambda_execution-role_068103E3.name}" + } + }, + "aws_lambda_alias": { + "test-versioned-lambda_alias_60AF9CE1": { + "depends_on": [ + "aws_lambda_function.test-versioned-lambda_D818669D" + ], + "function_name": "\${aws_lambda_function.test-versioned-lambda_D818669D.function_name}", + "function_version": "\${element(split(\\":\\", aws_lambda_function.test-versioned-lambda_D818669D.qualified_arn), 7)}", + "lifecycle": { + "ignore_changes": [ + "function_version" + ] + }, + "name": "DEPLOYED" + } + }, + "aws_lambda_function": { + "test-versioned-lambda_D818669D": { + "filename": "\${data.archive_file.test-versioned-lambda_lambda-default-file_E2490368.output_path}", + "function_name": "Test-Lambda-Function", + "handler": "index.handler", + "lifecycle": { + "ignore_changes": [ + "filename", + "source_code_hash" + ] + }, + "memory_size": 128, + "publish": true, + "reserved_concurrent_executions": -1, + "role": "\${aws_iam_role.test-versioned-lambda_execution-role_068103E3.arn}", + "runtime": "python3.8", + "source_code_hash": "\${data.archive_file.test-versioned-lambda_lambda-default-file_E2490368.output_base64sha256}", + "tags": { + "for": "test", + "my": "tag" + }, + "timeout": 5 + } + }, + "aws_s3_bucket": { + "test-versioned-lambda_code-bucket_C658EE92": { + "bucket": "test-bucket", + "force_destroy": true, + "tags": { + "for": "test", + "my": "tag" + } + } + }, + "aws_s3_bucket_acl": { + "test-versioned-lambda_code-bucket-acl_3407F3AC": { + "acl": "private", + "bucket": "\${aws_s3_bucket.test-versioned-lambda_code-bucket_C658EE92.id}", + "depends_on": [ + "aws_s3_bucket_ownership_controls.test-versioned-lambda_code-bucket-ownership-controls_7D2C8F19" + ] + } + }, + "aws_s3_bucket_ownership_controls": { + "test-versioned-lambda_code-bucket-ownership-controls_7D2C8F19": { + "bucket": "\${aws_s3_bucket.test-versioned-lambda_code-bucket_C658EE92.id}", + "rule": { + "object_ownership": "BucketOwnerPreferred" + } + } + }, + "aws_s3_bucket_public_access_block": { + "test-versioned-lambda_code-bucket-public-access-block_9662548B": { + "block_public_acls": true, + "block_public_policy": true, + "bucket": "\${aws_s3_bucket.test-versioned-lambda_code-bucket_C658EE92.id}" + } + } + } +}" +`; + +exports[`ApplicationVersionedLambda renders a versioned lambda with timeout 1`] = ` +"{ + "data": { + "archive_file": { + "test-versioned-lambda_lambda-default-file_E2490368": { + "output_path": "index.py.zip", + "source": [ + { + "content": "import json\\ndef handler(event, context):\\n\\t print(event)\\n\\t return {'statusCode': 200, 'headers': {'dance': 'party'}, 'body': json.dumps({'electric': 'boogaloo'}), 'isBase64Encoded': False}", + "filename": "index.py" + } + ], + "type": "zip" + } + }, + "aws_iam_policy_document": { + "test-versioned-lambda_assume-policy-document_54E2E6E3": { + "statement": [ + { + "actions": [ + "sts:AssumeRole" + ], + "effect": "Allow", + "principals": [ + { + "identifiers": [ + "lambda.amazonaws.com", + "edgelambda.amazonaws.com" + ], + "type": "Service" + } + ] + } + ], + "version": "2012-10-17" + }, + "test-versioned-lambda_execution-policy-document_BA37CD4D": { + "statement": [ + { + "actions": [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents", + "logs:DescribeLogStreams" + ], + "effect": "Allow", + "resources": [ + "arn:aws:logs:*:*:*" + ] + } + ], + "version": "2012-10-17" + } + } + }, + "resource": { + "aws_cloudwatch_log_group": { + "test-versioned-lambda_log-group_CB8A67E7": { + "depends_on": [ + "aws_lambda_function.test-versioned-lambda_D818669D" + ], + "name": "/aws/lambda/\${aws_lambda_function.test-versioned-lambda_D818669D.function_name}", + "retention_in_days": 14 + } + }, + "aws_iam_policy": { + "test-versioned-lambda_execution-policy_A934214F": { + "name": "Test-Lambda-ExecutionRolePolicy", + "policy": "\${data.aws_iam_policy_document.test-versioned-lambda_execution-policy-document_BA37CD4D.json}" + } + }, + "aws_iam_role": { + "test-versioned-lambda_execution-role_068103E3": { + "assume_role_policy": "\${data.aws_iam_policy_document.test-versioned-lambda_assume-policy-document_54E2E6E3.json}", + "name": "Test-Lambda-ExecutionRole" + } + }, + "aws_iam_role_policy_attachment": { + "test-versioned-lambda_execution-role-policy-attachment_30E5BCA4": { + "depends_on": [ + "aws_iam_role.test-versioned-lambda_execution-role_068103E3", + "aws_iam_policy.test-versioned-lambda_execution-policy_A934214F" + ], + "policy_arn": "\${aws_iam_policy.test-versioned-lambda_execution-policy_A934214F.arn}", + "role": "\${aws_iam_role.test-versioned-lambda_execution-role_068103E3.name}" + } + }, + "aws_lambda_alias": { + "test-versioned-lambda_alias_60AF9CE1": { + "depends_on": [ + "aws_lambda_function.test-versioned-lambda_D818669D" + ], + "function_name": "\${aws_lambda_function.test-versioned-lambda_D818669D.function_name}", + "function_version": "\${element(split(\\":\\", aws_lambda_function.test-versioned-lambda_D818669D.qualified_arn), 7)}", + "lifecycle": { + "ignore_changes": [ + "function_version" + ] + }, + "name": "DEPLOYED" + } + }, + "aws_lambda_function": { + "test-versioned-lambda_D818669D": { + "filename": "\${data.archive_file.test-versioned-lambda_lambda-default-file_E2490368.output_path}", + "function_name": "Test-Lambda-Function", + "handler": "index.handler", + "lifecycle": { + "ignore_changes": [ + "filename", + "source_code_hash" + ] + }, + "memory_size": 128, + "publish": true, + "reserved_concurrent_executions": -1, + "role": "\${aws_iam_role.test-versioned-lambda_execution-role_068103E3.arn}", + "runtime": "python3.8", + "source_code_hash": "\${data.archive_file.test-versioned-lambda_lambda-default-file_E2490368.output_base64sha256}", + "timeout": 500 + } + }, + "aws_s3_bucket": { + "test-versioned-lambda_code-bucket_C658EE92": { + "bucket": "test-bucket", + "force_destroy": true + } + }, + "aws_s3_bucket_acl": { + "test-versioned-lambda_code-bucket-acl_3407F3AC": { + "acl": "private", + "bucket": "\${aws_s3_bucket.test-versioned-lambda_code-bucket_C658EE92.id}", + "depends_on": [ + "aws_s3_bucket_ownership_controls.test-versioned-lambda_code-bucket-ownership-controls_7D2C8F19" + ] + } + }, + "aws_s3_bucket_ownership_controls": { + "test-versioned-lambda_code-bucket-ownership-controls_7D2C8F19": { + "bucket": "\${aws_s3_bucket.test-versioned-lambda_code-bucket_C658EE92.id}", + "rule": { + "object_ownership": "BucketOwnerPreferred" + } + } + }, + "aws_s3_bucket_public_access_block": { + "test-versioned-lambda_code-bucket-public-access-block_9662548B": { + "block_public_acls": true, + "block_public_policy": true, + "bucket": "\${aws_s3_bucket.test-versioned-lambda_code-bucket_C658EE92.id}" + } + } + } +}" +`; + +exports[`ApplicationVersionedLambda renders a versioned lambda with vpc 1`] = ` +"{ + "data": { + "archive_file": { + "test-versioned-lambda_lambda-default-file_E2490368": { + "output_path": "index.py.zip", + "source": [ + { + "content": "import json\\ndef handler(event, context):\\n\\t print(event)\\n\\t return {'statusCode': 200, 'headers': {'dance': 'party'}, 'body': json.dumps({'electric': 'boogaloo'}), 'isBase64Encoded': False}", + "filename": "index.py" + } + ], + "type": "zip" + } + }, + "aws_iam_policy_document": { + "test-versioned-lambda_assume-policy-document_54E2E6E3": { + "statement": [ + { + "actions": [ + "sts:AssumeRole" + ], + "effect": "Allow", + "principals": [ + { + "identifiers": [ + "lambda.amazonaws.com", + "edgelambda.amazonaws.com" + ], + "type": "Service" + } + ] + } + ], + "version": "2012-10-17" + }, + "test-versioned-lambda_execution-policy-document_BA37CD4D": { + "statement": [ + { + "actions": [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents", + "logs:DescribeLogStreams" + ], + "effect": "Allow", + "resources": [ + "arn:aws:logs:*:*:*" + ] + }, + { + "actions": [ + "ec2:DescribeNetworkInterfaces", + "ec2:CreateNetworkInterface", + "ec2:DeleteNetworkInterface", + "ec2:DescribeInstances", + "ec2:AttachNetworkInterface" + ], + "effect": "Allow", + "resources": [ + "*" + ] + } + ], + "version": "2012-10-17" + } + } + }, + "resource": { + "aws_cloudwatch_log_group": { + "test-versioned-lambda_log-group_CB8A67E7": { + "depends_on": [ + "aws_lambda_function.test-versioned-lambda_D818669D" + ], + "name": "/aws/lambda/\${aws_lambda_function.test-versioned-lambda_D818669D.function_name}", + "retention_in_days": 14 + } + }, + "aws_iam_policy": { + "test-versioned-lambda_execution-policy_A934214F": { + "name": "Test-Lambda-ExecutionRolePolicy", + "policy": "\${data.aws_iam_policy_document.test-versioned-lambda_execution-policy-document_BA37CD4D.json}" + } + }, + "aws_iam_role": { + "test-versioned-lambda_execution-role_068103E3": { + "assume_role_policy": "\${data.aws_iam_policy_document.test-versioned-lambda_assume-policy-document_54E2E6E3.json}", + "name": "Test-Lambda-ExecutionRole" + } + }, + "aws_iam_role_policy_attachment": { + "test-versioned-lambda_execution-role-policy-attachment_30E5BCA4": { + "depends_on": [ + "aws_iam_role.test-versioned-lambda_execution-role_068103E3", + "aws_iam_policy.test-versioned-lambda_execution-policy_A934214F" + ], + "policy_arn": "\${aws_iam_policy.test-versioned-lambda_execution-policy_A934214F.arn}", + "role": "\${aws_iam_role.test-versioned-lambda_execution-role_068103E3.name}" + } + }, + "aws_lambda_alias": { + "test-versioned-lambda_alias_60AF9CE1": { + "depends_on": [ + "aws_lambda_function.test-versioned-lambda_D818669D" + ], + "function_name": "\${aws_lambda_function.test-versioned-lambda_D818669D.function_name}", + "function_version": "\${element(split(\\":\\", aws_lambda_function.test-versioned-lambda_D818669D.qualified_arn), 7)}", + "lifecycle": { + "ignore_changes": [ + "function_version" + ] + }, + "name": "DEPLOYED" + } + }, + "aws_lambda_function": { + "test-versioned-lambda_D818669D": { + "filename": "\${data.archive_file.test-versioned-lambda_lambda-default-file_E2490368.output_path}", + "function_name": "Test-Lambda-Function", + "handler": "index.handler", + "lifecycle": { + "ignore_changes": [ + "filename", + "source_code_hash" + ] + }, + "memory_size": 128, + "publish": true, + "reserved_concurrent_executions": -1, + "role": "\${aws_iam_role.test-versioned-lambda_execution-role_068103E3.arn}", + "runtime": "python3.8", + "source_code_hash": "\${data.archive_file.test-versioned-lambda_lambda-default-file_E2490368.output_base64sha256}", + "timeout": 5, + "vpc_config": { + "security_group_ids": [ + "sec1", + "sec2" + ], + "subnet_ids": [ + "1", + "2" + ] + } + } + }, + "aws_s3_bucket": { + "test-versioned-lambda_code-bucket_C658EE92": { + "bucket": "test-bucket", + "force_destroy": true + } + }, + "aws_s3_bucket_acl": { + "test-versioned-lambda_code-bucket-acl_3407F3AC": { + "acl": "private", + "bucket": "\${aws_s3_bucket.test-versioned-lambda_code-bucket_C658EE92.id}", + "depends_on": [ + "aws_s3_bucket_ownership_controls.test-versioned-lambda_code-bucket-ownership-controls_7D2C8F19" + ] + } + }, + "aws_s3_bucket_ownership_controls": { + "test-versioned-lambda_code-bucket-ownership-controls_7D2C8F19": { + "bucket": "\${aws_s3_bucket.test-versioned-lambda_code-bucket_C658EE92.id}", + "rule": { + "object_ownership": "BucketOwnerPreferred" + } + } + }, + "aws_s3_bucket_public_access_block": { + "test-versioned-lambda_code-bucket-public-access-block_9662548B": { + "block_public_acls": true, + "block_public_policy": true, + "bucket": "\${aws_s3_bucket.test-versioned-lambda_code-bucket_C658EE92.id}" + } + } + } +}" +`; diff --git a/packages/terraform-modules/src/example.ts b/packages/terraform-modules/src/example.ts new file mode 100644 index 00000000..db7078e2 --- /dev/null +++ b/packages/terraform-modules/src/example.ts @@ -0,0 +1,170 @@ +import { Construct } from 'constructs'; +import { App, TerraformStack } from 'cdktf'; +import { AwsProvider } from '@cdktf/provider-aws/lib/provider'; +import { PocketALBApplication } from './pocket/PocketALBApplication'; +import { ApplicationECSContainerDefinitionProps } from './base/ApplicationECSContainerDefinition'; +import { LocalProvider } from '@cdktf/provider-local/lib/provider'; +import { NullProvider } from '@cdktf/provider-null/lib/provider'; +import { TimeProvider } from '@cdktf/provider-time/lib/provider'; +import { Wafv2WebAcl } from '@cdktf/provider-aws/lib/wafv2-web-acl'; +import { PocketAwsSyntheticChecks } from './pocket/PocketCloudwatchSynthetics'; +import { PocketVPC } from './pocket/PocketVPC'; + +class Example extends TerraformStack { + constructor(scope: Construct, name: string) { + super(scope, name); + + new AwsProvider(this, 'aws', { + region: 'us-east-1', + }); + new LocalProvider(this, 'local', {}); + new NullProvider(this, 'null', {}); + new TimeProvider(this, 'timeProvider', {}); + + const pocketVpc = new PocketVPC(this, 'pocket-vpc'); + + const containerConfigBlue: ApplicationECSContainerDefinitionProps = { + name: 'blueContainer', + containerImage: 'n0coast/node-example', + repositoryCredentialsParam: + 'arn:aws:secretsmanager:us-east-1:410318598490:secret:Shared/DockerHub-79jJxy', + portMappings: [ + { + hostPort: 3000, + containerPort: 3000, + }, + ], + envVars: [ + { + name: 'foo', + value: 'bar', + }, + ], + mountPoints: [ + { + containerPath: '/qdrant/storage', + sourceVolume: 'data', + }, + ], + logMultilinePattern: '^\\S.+', + logDatetimeFormat: '[%b %d, %Y %H:%M:%S]', + // logGroup: '/platform/blueContainer/ecs', use logGroup OR logDatetimeFormat + logStreamPrefix: 'blueContainer', + ulimits: [ + { + hardLimit: 65535, + name: 'nofile', + softLimit: 65535, + }, + ], + }; + + const wafAcl = new Wafv2WebAcl(this, 'example_waf_acl', { + description: 'Example Pocket Waf ACL', + name: 'pocket-example-waf', + scope: 'REGIONAL', + defaultAction: { + allow: {}, + }, + visibilityConfig: { + cloudwatchMetricsEnabled: true, + metricName: 'pocket-example-waf-default-rule', + sampledRequestsEnabled: true, + }, + rule: [ + { + name: 'ExampleRateBasedPolicy', + priority: 1, + action: { + block: {}, + }, + statement: { + rateBasedStatement: { + limit: 10000, + aggregateKeyType: 'IP', + }, + }, + visibilityConfig: { + cloudwatchMetricsEnabled: true, + metricName: 'pocket-example-waf-rate-limit', + sampledRequestsEnabled: true, + }, + }, + ], + }); + + new PocketALBApplication(this, 'example', { + accessLogs: { + bucket: 'pocket-dev-blah', + }, + alb6CharacterPrefix: 'ACMECO', + cdn: false, // maybe make this false if you're testing an actual terraform apply - cdn's take a loooong time to spin up + codeDeploy: { + useCodeDeploy: true, + }, + containerConfigs: [containerConfigBlue], + domain: 'acme.getpocket.dev', + ecsIamConfig: { + prefix: 'ACME-Dev', + taskExecutionRolePolicyStatements: [ + { + effect: 'Allow', + actions: [ + 'secretsmanager:GetResourcePolicy', + 'secretsmanager:GetSecretValue', + 'secretsmanager:DescribeSecret', + 'secretsmanager:ListSecretVersionIds', + ], + resources: [ + 'arn:aws:secretsmanager:us-east-1:410318598490:secret:Shared/DockerHub-79jJxy', + ], + }, + ], + taskRolePolicyStatements: [], + taskExecutionDefaultAttachmentArn: + 'arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy', + }, + efsConfig: { + creationToken: 'ACME-Dev', + volumeName: 'data', + }, + exposedContainer: { + name: 'blueContainer', + port: 3000, + healthCheckPath: '/', + }, + internal: false, + prefix: 'ACME-Dev', // Prefix is a combo of the `Name-Environment` + wafConfig: { + aclArn: wafAcl.arn, + }, + }); + + new PocketAwsSyntheticChecks(this, 'synthetics', { + environment: 'Dev', + prefix: 'ACME-Dev', + query: [ + { + endpoint: 'acme.getpocket.dev', + data: '{"query": "query { someGraphQlQuery(arg1: \\"1\\", arg2: \\"1\\") {returnedAttr} }"}', + jmespath: 'errors[0].message', // errors checks can confirm GraphQL is working as desired, though preferably these are positive checks + response: + 'Error - Not Found: A resource by that arg1 could not be found', + }, + ], + securityGroupIds: pocketVpc.defaultSecurityGroups.ids, + shortName: 'ACME', + subnetIds: pocketVpc.privateSubnetIds, + uptime: [ + { + response: 'ok', + url: `acme.getpocket.dev/.well-known/apollo/server-health`, + }, + ], + }); + } +} + +const app = new App(); +new Example(app, 'acme-example'); +app.synth(); diff --git a/packages/terraform-modules/src/index.ts b/packages/terraform-modules/src/index.ts new file mode 100644 index 00000000..80612535 --- /dev/null +++ b/packages/terraform-modules/src/index.ts @@ -0,0 +1,37 @@ +// this is the entry point for the npm package +// anything we want consumable (module, type, class, etc) should be exported here +export * from './pocket/PocketApiGatewayLambdaIntegration'; +export * from './pocket/PocketALBApplication'; +export * from './pocket/PocketCloudwatchSynthetics'; +export * from './pocket/PocketECSApplication'; +export * from './pocket/PocketECSCodePipeline'; +export * from './pocket/PocketEventBridgeRuleWithMultipleTargets'; +export * from './pocket/PocketEventBridgeWithLambdaTarget'; +export * from './pocket/PocketPagerDuty'; +export * from './pocket/PocketSQSWithLambdaTarget'; +export * from './pocket/PocketVersionedLambda'; +export * from './pocket/PocketVPC'; +export * from './pocket/PocketSynthetics'; +export * from './base/ApplicationAutoscaling'; +export * from './base/ApplicationBaseDNS'; +export * from './base/ApplicationCertificate'; +export * from './base/ApplicationDynamoDBTable'; +export * from './base/ApplicationECR'; +export * from './base/ApplicationECSAlbCodeDeploy'; +export * from './base/ApplicationECSCluster'; +export * from './base/ApplicationECSContainerDefinition'; +export * from './base/ApplicationECSIAM'; +export * from './base/ApplicationECSService'; +export * from './base/ApplicationElasticacheCluster'; +export * from './base/ApplicationEventBridgeRule'; +export * from './base/ApplicationEventBus'; +export * from './base/ApplicationLambdaCodeDeploy'; +export * from './base/ApplicationLoadBalancer'; +export * from './base/ApplicationMemcache'; +export * from './base/ApplicationRDSCluster'; +export * from './base/ApplicationRedis'; +export * from './base/ApplicationSQSQueue'; +export * from './base/ApplicationSqsSnsTopicSubscription'; +export * from './base/ApplicationTargetGroup'; +export * from './base/ApplicationVersionedLambda'; +export * from './base/ApplicationLambdaSnsTopicSubscription'; diff --git a/packages/terraform-modules/src/pocket/PocketALBApplication.spec.ts b/packages/terraform-modules/src/pocket/PocketALBApplication.spec.ts new file mode 100644 index 00000000..5cfc469e --- /dev/null +++ b/packages/terraform-modules/src/pocket/PocketALBApplication.spec.ts @@ -0,0 +1,429 @@ +import { TerraformStack, Testing } from 'cdktf'; +import { + PocketALBApplication, + PocketALBApplicationProps, +} from './PocketALBApplication'; + +describe('PocketALBApplication', () => { + let BASE_CONFIG: PocketALBApplicationProps; + + beforeEach(() => { + BASE_CONFIG = { + prefix: 'testapp', + alb6CharacterPrefix: 'TSTAPP', + domain: 'testing.bowling.gov', + containerConfigs: [], + codeDeploy: { + useCodeDeploy: false, + snsNotificationTopicArn: 'notify-me', + }, + ecsIamConfig: { + prefix: 'testapp-', + taskExecutionDefaultAttachmentArn: '', + taskExecutionRolePolicyStatements: [], + taskRolePolicyStatements: [], + }, + exposedContainer: { + name: 'main_container', + port: 8675309, + healthCheckPath: '/test', + }, + }; + }); + + it('renders an application with minimal config', () => { + const synthed = Testing.synthScope((stack) => { + new PocketALBApplication(stack, 'testPocketApp', BASE_CONFIG); + }); + expect(synthed).toMatchSnapshot(); + }); + + it('renders an external application', () => { + const synthed = Testing.synthScope((stack) => { + BASE_CONFIG.internal = false; + BASE_CONFIG.cdn = true; + + new PocketALBApplication(stack, 'testPocketApp', BASE_CONFIG); + }); + expect(synthed).toMatchSnapshot(); + }); + + it('renders an internal application', () => { + const synthed = Testing.synthScope((stack) => { + BASE_CONFIG.internal = true; + BASE_CONFIG.cdn = false; + + new PocketALBApplication(stack, 'testPocketApp', BASE_CONFIG); + }); + expect(synthed).toMatchSnapshot(); + }); + + it('renders an application with custom task sizes', () => { + const synthed = Testing.synthScope((stack) => { + BASE_CONFIG.taskSize = { + cpu: 8675, + memory: 309, + }; + + new PocketALBApplication(stack, 'testPocketApp', BASE_CONFIG); + }); + expect(synthed).toMatchSnapshot(); + }); + + it('throws an error trying for an internal app with a cdn', () => { + const app = Testing.app(); + const stack = new TerraformStack(app, 'test'); + + BASE_CONFIG.internal = true; + BASE_CONFIG.cdn = true; + + expect(() => { + new PocketALBApplication(stack, 'testPocketApp', BASE_CONFIG); + }).toThrow('You can not have a cached ALB and have it be internal.'); + }); + + it('renders an internal application with tags', () => { + const synthed = Testing.synthScope((stack) => { + BASE_CONFIG.internal = true; + BASE_CONFIG.cdn = false; + BASE_CONFIG.tags = { + name: 'thedude', + hobby: 'bowling', + }; + + new PocketALBApplication(stack, 'testPocketApp', BASE_CONFIG); + }); + expect(synthed).toMatchSnapshot(); + }); + + it('renders an application with autoscaling group and tags', () => { + const synthed = Testing.synthScope((stack) => { + BASE_CONFIG.tags = { + name: 'thedude', + hobby: 'bowling', + }; + + BASE_CONFIG.autoscalingConfig = { + targetMinCapacity: 1, + targetMaxCapacity: 2, + stepScaleInAdjustment: -1, + stepScaleOutAdjustment: 2, + scaleInThreshold: 30, + scaleOutThreshold: 45, + }; + + new PocketALBApplication(stack, 'testPocketApp', BASE_CONFIG); + }); + expect(synthed).toMatchSnapshot(); + }); + + it('renders an application with default autoscaling group and tags', () => { + const synthed = Testing.synthScope((stack) => { + BASE_CONFIG.tags = { + name: 'thedude', + hobby: 'bowling', + }; + + new PocketALBApplication(stack, 'testPocketApp', BASE_CONFIG); + }); + expect(synthed).toMatchSnapshot(); + }); + + it('renders an application with modified container def protocol, cpu and memory reservation', () => { + const synthed = Testing.synthScope((stack) => { + BASE_CONFIG.containerConfigs = [ + { + name: 'xray-daemon', + portMappings: [ + { + hostPort: 0, + containerPort: 2000, + protocol: 'udp', + }, + ], + cpu: 10, + memoryReservation: 50, + }, + ]; + + new PocketALBApplication(stack, 'testPocketApp', BASE_CONFIG); + }); + expect(synthed).toMatchSnapshot(); + }); + + it('renders an application alarms', () => { + const synthed = Testing.synthScope((stack) => { + const alarmConfig = { + ...BASE_CONFIG, + alarms: { + http5xxError: { + threshold: 10, + evaluationPeriods: 1, + period: 600, + actions: ['sns-arn-for-5xx-errors'], + }, + httpLatency: { + threshold: 0.5, + evaluationPeriods: 2, + period: 300, + actions: ['sns-arn-for-latency'], + }, + httpRequestCount: { + threshold: 10000, + evaluationPeriods: 3, + period: 900, + actions: [], + }, + }, + }; + + new PocketALBApplication(stack, 'testPocketApp', alarmConfig); + }); + expect(synthed).toMatchSnapshot(); + }); + + it('renders an application custom default alarms', () => { + const synthed = Testing.synthScope((stack) => { + const alarmConfig = { + ...BASE_CONFIG, + alarms: { + customAlarms: [ + { + alarmName: `Alarm-Custom`, + namespace: 'TM/Alarm', + metricName: 'Custom', + dimensions: { Test: 'Alarm' }, + period: 300, + statistic: 'Sum', + comparisonOperator: 'GreaterThanThreshold', + threshold: 500, + evaluationPeriods: 1, + }, + ], + }, + }; + + new PocketALBApplication(stack, 'testPocketApp', alarmConfig); + }); + expect(synthed).toMatchSnapshot(); + }); + + it('validates http 5xx alarm config', () => { + const app = Testing.app(); + const stack = new TerraformStack(app, 'test'); + + const alarmConfig = { + ...BASE_CONFIG, + alarms: { + http5xxErrorPercentage: { + datapointsToAlarm: 2, + }, + }, + }; + + expect( + () => new PocketALBApplication(stack, 'testPocketApp', alarmConfig), + ).toThrow(Error); + }); + + it('validates http latency alarm config', () => { + const app = Testing.app(); + const stack = new TerraformStack(app, 'test'); + + const alarmConfig = { + ...BASE_CONFIG, + alarms: { + httpLatency: { + datapointsToAlarm: 2, + }, + }, + }; + + expect( + () => new PocketALBApplication(stack, 'testPocketApp', alarmConfig), + ).toThrow(Error); + }); + + it('validates custom alarms config', () => { + const app = Testing.app(); + const stack = new TerraformStack(app, 'test'); + + const alarmConfig = { + ...BASE_CONFIG, + alarms: { + customAlarms: [ + { + alarmName: `Test-Alarm-Custom`, + namespace: 'TM/Alarm', + metricName: 'Custom', + dimensions: { Test: 'Alarm' }, + period: 300, + statistic: 'Sum', + comparisonOperator: 'GreaterThanThreshold', + threshold: 500, + evaluationPeriods: 1, + datapointsToAlarm: 2, + }, + ], + }, + }; + + expect( + () => new PocketALBApplication(stack, 'testPocketApp', alarmConfig), + ).toThrow(Error); + }); + + it('exposes the alb listeners', () => { + const app = Testing.app(); + const stack = new TerraformStack(app, 'test'); + + const pocketApp = new PocketALBApplication( + stack, + 'testPocketApp', + BASE_CONFIG, + ); + + expect(pocketApp.listeners.length).toEqual(2); + // We are using "portInput" here because there is a bug + // when the "port" attribute is used, it returns a random + // and really high floating point number + // Example below: + // Expected: 80 + // Received: -1.8881545897087505e+289 + expect(pocketApp.listeners[0].portInput).toEqual(80); + expect(pocketApp.listeners[1].portInput).toEqual(443); + }); + + it('exposes the ecs service', () => { + const app = Testing.app(); + const stack = new TerraformStack(app, 'test'); + + const pocketApp = new PocketALBApplication( + stack, + 'testPocketApp', + BASE_CONFIG, + ); + + expect(pocketApp.ecsService.mainTargetGroup).not.toBeNull(); + }); + + it('renders an Pocket App with code deploy', () => { + const synthed = Testing.synthScope((stack) => { + new PocketALBApplication(stack, 'testPocketApp', { + ...BASE_CONFIG, + codeDeploy: { + useCodeDeploy: true, + }, + }); + }); + expect(synthed).toMatchSnapshot(); + }); + + it('renders an Pocket App with code deploy notifications set to failed only', () => { + const synthed = Testing.synthScope((stack) => { + new PocketALBApplication(stack, 'testPocketApp', { + ...BASE_CONFIG, + codeDeploy: { + useCodeDeploy: true, + notifications: { + notifyOnFailed: true, + notifyOnStarted: false, + notifyOnSucceeded: false, + }, + }, + }); + }); + expect(synthed).toMatchSnapshot(); + }); + + it('renders an Pocket App with code deploy and creates appspec.json and taskdef.json when using code pipeline', () => { + const synthed = Testing.synthScope((stack) => { + new PocketALBApplication(stack, 'testPocketApp', { + ...BASE_CONFIG, + codeDeploy: { + useCodePipeline: true, + useCodeDeploy: true, + }, + }); + }); + expect(synthed).toMatchSnapshot(); + }); + + it('renders an Pocket App with logs and dashboard in a specified region', () => { + const synthed = Testing.synthScope((stack) => { + new PocketALBApplication(stack, 'testPocketApp', { + ...BASE_CONFIG, + region: 'central region', + }); + }); + expect(synthed).toMatchSnapshot(); + }); + + it('renders an Pocket App with custom Alarm Description', () => { + const synthed = Testing.synthScope((stack) => { + new PocketALBApplication(stack, 'testPocketApp', { + ...BASE_CONFIG, + alarms: { + http5xxErrorPercentage: { + alarmDescription: 'Uh oh. something bad happened.', + }, + httpLatency: { + alarmDescription: 'Uh oh. This is very slow.', + }, + }, + }); + }); + expect(synthed).toMatchSnapshot(); + }); + + it('renders a Pocket App with attached persistent storage', () => { + const synthed = Testing.synthScope((stack) => { + new PocketALBApplication(stack, 'testPocketApp', { + ...BASE_CONFIG, + efsConfig: { + volumeName: 'sourceVolume', + creationToken: 'someToken', + }, + containerConfigs: [ + { + name: 'someMountPoint', + mountPoints: [ + { + containerPath: '/qdrant/storage', + sourceVolume: 'sourceVolume', + }, + ], + }, + ], + }); + }); + expect(synthed).toMatchSnapshot(); + }); + + it('throws an error for if you enable a CDN and a WAF', () => { + const app = Testing.app(); + const stack = new TerraformStack(app, 'test'); + + expect(() => { + new PocketALBApplication(stack, 'testPocketApp', { + ...BASE_CONFIG, + wafConfig: { aclArn: 'something' }, + cdn: true, + }); + }).toThrow( + 'Implementation of waf association with CDN is not currently supported', + ); + }); + + it('renders a Pocket App with attached waf', () => { + const synthed = Testing.synthScope((stack) => { + new PocketALBApplication(stack, 'testPocketApp', { + ...BASE_CONFIG, + wafConfig: { + aclArn: 'justAString', + }, + }); + }); + expect(synthed).toMatchSnapshot(); + }); +}); diff --git a/packages/terraform-modules/src/pocket/PocketALBApplication.ts b/packages/terraform-modules/src/pocket/PocketALBApplication.ts new file mode 100644 index 00000000..5abd2bd7 --- /dev/null +++ b/packages/terraform-modules/src/pocket/PocketALBApplication.ts @@ -0,0 +1,1016 @@ +import { AlbListener } from '@cdktf/provider-aws/lib/alb-listener'; +import { CloudfrontDistribution } from '@cdktf/provider-aws/lib/cloudfront-distribution'; +import { CloudwatchDashboard } from '@cdktf/provider-aws/lib/cloudwatch-dashboard'; +import { + CloudwatchMetricAlarm, + CloudwatchMetricAlarmConfig, +} from '@cdktf/provider-aws/lib/cloudwatch-metric-alarm'; +import { EfsFileSystem } from '@cdktf/provider-aws/lib/efs-file-system'; +import { AwsProvider } from '@cdktf/provider-aws/lib/provider'; +import { Route53Record } from '@cdktf/provider-aws/lib/route53-record'; +import { Wafv2WebAclAssociation } from '@cdktf/provider-aws/lib/wafv2-web-acl-association'; +import { TerraformMetaArguments } from 'cdktf'; +import { Construct } from 'constructs'; +import { + ApplicationAutoscaling, + ApplicationBaseDNS, + ApplicationCertificate, + ApplicationECSCluster, + ApplicationECSContainerDefinitionProps, + ApplicationECSIAMProps, + ApplicationECSService, + ApplicationECSServiceProps, + ApplicationLoadBalancer, +} from '..'; +import { PocketVPC } from './PocketVPC'; + +export interface PocketALBApplicationAlarmProps extends TerraformMetaArguments { + threshold?: number; + period?: number; + evaluationPeriods?: number; + datapointsToAlarm?: number; + actions?: string[]; + alarmDescription?: string; +} + +export interface PocketALBApplicationProps extends TerraformMetaArguments { + /** + * This is the prefix for the names of all the resources + * created by this construct. + */ + prefix: string; + /** + * Optional config to define the region for the service. + * This is used to define the cloudwatch dashboards + * as well as the region of the cloudwatch logs + */ + region?: string; + /** + * VPC configuration for all resource that require it within + * this construct. A default Pocket VPC will be used if not + * provided. + */ + vpcConfig?: { + vpcId: string; + privateSubnetIds: string[]; + publicSubnetIds: string[]; + }; + /** + * Prefix for the name of the application load balancer created + * as part of this construct. Due to an arbitrary AWS character + * limit, this has to be kept at 6 characters or less. + */ + alb6CharacterPrefix: string; + /** + * Optional config to create an internal or public facing + * ALB. By default, this is set to false. + */ + internal?: boolean; + /** + * The domain the ECS service created by this construct will + * be available at. + */ + domain: string; + /** + * Optional config to create a CDN. By default, no CDN is created. + */ + cdn?: boolean; + + /** + * Optional config to dump alb logs to a bucket. + + */ + accessLogs?: { + /** + * Existing bucket to dump alb logs too, one of existingBucket or bucket must be chosen. + * If using this options, this module assumes that the bucket already exists in your AWS account and has IAM setup according + * to https://docs.aws.amazon.com/elasticloadbalancing/latest/application/enable-access-logging.html#attach-bucket-policy which is account wide. + */ + existingBucket?: string; + + /** + * Bucket to dump alb logs too, one of existingBucket or bucket must be chosen. + */ + bucket?: string; + + /** + * Optional bucket path prefix. If not defined will use server-logs/{service-name}/internal-alb/AWSLogs/{awsaccountid}/elasticloadbalancing/ + * Be sure to include a trailing / + */ + prefix?: string; + }; + + /** + * Option for how the service created by this construct should be + * deployed. + */ + codeDeploy: { + /** + * Option to create a CodePipeline resource. Default is false. + */ + useCodePipeline?: boolean; + /** + * Option to create a CodeDeploy application. + */ + useCodeDeploy: boolean; + /** + * Option to deploy a new version using terraform, instead of externally + */ + useTerraformBasedCodeDeploy?: boolean; + /** + * Optional SNS topic for CodeDeploy notifications. + */ + snsNotificationTopicArn?: string; + /** + * Optional wait time after replacement task completion. Default is 5 minutes. + */ + successTerminationWaitTimeInMinutes?: number; + + notifications?: { + /** + * Option to send CodeDeploy notifications on Started event, defaults to true. + */ + notifyOnStarted?: boolean; + /** + * Option to send CodeDeploy notifications on Succeeded event, defaults to true. + */ + notifyOnSucceeded?: boolean; + /** + * Option to send CodeDeploy notifications on Failed event, defaults to true. + */ + notifyOnFailed?: boolean; + }; + }; + /** + * Tags for all resources created by this construct. + */ + tags?: { [key: string]: string }; + /** + * Container definitions for the ECS task. + */ + containerConfigs: ApplicationECSContainerDefinitionProps[]; + /** + * Description of the container that is exposed to the ALB. + */ + exposedContainer: { + port: number; + name: string; + healthCheckPath: string; + }; + /** + * ECS task size configuration. + */ + taskSize?: { + cpu: number; + memory: number; + }; + /** + * IAM config for the ECS Service and Tasks. + */ + ecsIamConfig: ApplicationECSIAMProps; + /** + * Options for configuring the autoscaling policy for + * the ECS service created by this construct. + */ + autoscalingConfig?: { + targetMinCapacity?: number; + targetMaxCapacity?: number; + stepScaleInAdjustment?: number; + stepScaleOutAdjustment?: number; + scaleInThreshold?: number; + scaleOutThreshold?: number; + }; + /** + * Option for defining Cloudwatch alarms + */ + alarms?: { + http5xxErrorPercentage?: PocketALBApplicationAlarmProps; + httpLatency?: PocketALBApplicationAlarmProps; + customAlarms?: CloudwatchMetricAlarmConfig[]; + }; + efsConfig?: { + creationToken?: string; + throughputMode?: string; + volumeName: string; + }; + wafConfig?: { + aclArn: string; + }; +} + +interface CreateALBReturn { + alb: ApplicationLoadBalancer; + albRecord: Route53Record; + albCertificate: ApplicationCertificate; +} + +// can export if we need to use outside of this module +const DEFAULT_AUTOSCALING_CONFIG = { + scaleOutThreshold: 45, + scaleInThreshold: 30, + targetMinCapacity: 1, + targetMaxCapacity: 2, + stepScaleInAdjustment: -1, + stepScaleOutAdjustment: 2, +}; + +export class PocketALBApplication extends Construct { + public readonly alb: ApplicationLoadBalancer; + public readonly ecsService: ApplicationECSService; + public readonly baseDNS: ApplicationBaseDNS; + public readonly listeners: AlbListener[]; + private readonly config: PocketALBApplicationProps; + private readonly pocketVPC: PocketALBApplicationProps['vpcConfig']; + private readonly efs: EfsFileSystem; + + constructor( + scope: Construct, + name: string, + config: PocketALBApplicationProps, + ) { + super(scope, name); + + this.listeners = []; + + this.config = PocketALBApplication.validateConfig(config); + + // use default auto-scaling config, but update any user-provided values + this.config.autoscalingConfig = { + ...DEFAULT_AUTOSCALING_CONFIG, + ...config.autoscalingConfig, + }; + + this.pocketVPC = this.getVpcConfig(config); + + //Set up the Base DNS stack for our application which includes a hosted SubZone + this.baseDNS = new ApplicationBaseDNS(this, `base_dns`, { + domain: config.domain, + tags: config.tags, + }); + + const { alb, albRecord, albCertificate } = this.createALB(); + this.alb = alb; + + if (config.cdn) { + this.createCDN(albRecord); + } + + if (config.wafConfig) { + if (config.cdn) { + throw new Error( + 'Implementation of waf association with CDN is not currently supported', + ); + } + this.createWAF(alb, config.wafConfig.aclArn); + } + + if (config.efsConfig) { + this.efs = this.createEfs(config); + } + + const ecsService = this.createECSService(alb, albCertificate); + this.ecsService = ecsService.ecs; + + this.createCloudwatchDashboard( + alb.alb.arnSuffix, + ecsService.ecs.service.name, + ecsService.cluster.cluster.name, + ); + + this.createCloudwatchAlarms(); + } + + /** + * Fall back to standard Pocket config if a custom VPC configuration has not been supplied. + * + * @param config + * @private + */ + private getVpcConfig( + config: PocketALBApplicationProps, + ): PocketALBApplicationProps['vpcConfig'] { + if (config.vpcConfig !== undefined) { + return { + vpcId: config.vpcConfig.vpcId, + privateSubnetIds: config.vpcConfig.privateSubnetIds, + publicSubnetIds: config.vpcConfig.publicSubnetIds, + }; + } else { + const pocketVpc = new PocketVPC( + this, + `pocket_vpc`, + config.provider as AwsProvider, + ); + return { + vpcId: pocketVpc.vpc.id, + privateSubnetIds: pocketVpc.privateSubnetIds, + publicSubnetIds: pocketVpc.publicSubnetIds, + }; + } + } + + /** + * Validate the Configuration + * + * @param config + * @private + */ + private static validateConfig( + config: PocketALBApplicationProps, + ): PocketALBApplicationProps { + config = PocketALBApplication.validateCachedALB(config); + + PocketALBApplication.validateAlarmsConfig(config.alarms); + + return config; + } + + private static validateAlarmsConfig( + config: PocketALBApplicationProps['alarms'], + ): void { + if (!config) return; + + const alarmsToValidate = { + http5xxErrorPercentage: 'HTTP 5xx Error', + httpLatency: 'HTTP Latency', + httpRequestCount: 'HTTP Request Count', + }; + + const errorMessage = + 'DatapointsToAlarm must be less than or equal to EvaluationPeriods'; + + Object.keys(alarmsToValidate).forEach((key) => { + if ( + config[key]?.datapointsToAlarm > (config[key]?.evaluationPeriods ?? 1) + ) { + throw new Error(`${alarmsToValidate[key]} Alarm: ${errorMessage}`); + } + }); + + config.customAlarms?.forEach((alarm: CloudwatchMetricAlarmConfig) => { + if (alarm.datapointsToAlarm > alarm.evaluationPeriods) { + throw new Error(`${alarm.alarmName}: ${errorMessage}`); + } + }); + } + + private createEfs(config: PocketALBApplicationProps): EfsFileSystem { + return new EfsFileSystem(this, 'efsFs', { + creationToken: config.efsConfig.creationToken, + encrypted: true, + tags: config.tags, + provider: config.provider, + }); + } + + private static validateCachedALB( + config: PocketALBApplicationProps, + ): PocketALBApplicationProps { + if (config.cdn === undefined) { + //Set a default of cached to false + config.cdn = false; + } + + if (config.internal === undefined) { + //Set a default of internal to false + config.internal = false; + } + + if (config.internal && config.cdn) { + throw Error('You can not have a cached ALB and have it be internal.'); + } + + return config; + } + + private createWAF(alb: ApplicationLoadBalancer, webAclArn: string) { + new Wafv2WebAclAssociation(this, 'application_waf_association', { + webAclArn: webAclArn, + resourceArn: alb.alb.arn, + }); + } + + /** + * Creates the ALB stack and certificates + * @private + */ + private createALB(): CreateALBReturn { + //Create our application Load Balancer + const alb = new ApplicationLoadBalancer(this, `application_load_balancer`, { + vpcId: this.pocketVPC.vpcId, + prefix: this.config.prefix, + alb6CharacterPrefix: this.config.alb6CharacterPrefix, + subnetIds: this.config.internal + ? this.pocketVPC.privateSubnetIds + : this.pocketVPC.publicSubnetIds, + internal: this.config.internal, + tags: this.config.tags, + accessLogs: this.config.accessLogs, + provider: this.config.provider, + }); + + //When the app uses a CDN we set the ALB to be direct.app-domain + //Then the CDN is our main app. + const albDomainName = this.config.cdn + ? `direct.${this.config.domain}` + : this.config.domain; + + //Sets up the record for the ALB. + const albRecord = new Route53Record(this, `alb_record`, { + name: albDomainName, + type: 'A', + zoneId: this.baseDNS.zoneId, + weightedRoutingPolicy: { + weight: 1, + }, + alias: { + name: alb.alb.dnsName, + zoneId: alb.alb.zoneId, + evaluateTargetHealth: true, + }, + lifecycle: { + ignoreChanges: ['weighted_routing_policy[0].weight'], + }, + setIdentifier: '1', + provider: this.config.provider, + }); + + //Creates the Certificate for the ALB + const albCertificate = new ApplicationCertificate(this, `alb_certificate`, { + zoneId: this.baseDNS.zoneId, + domain: albDomainName, + tags: this.config.tags, + provider: this.config.provider, + }); + + return { + alb, + albRecord, + albCertificate, + }; + } + + /** + * Create the CDN if the ALB is backed by one. + * + * @param albRecord + * @private + */ + private createCDN(albRecord: Route53Record): void { + //Create the certificate for the CDN + const cdnCertificate = new ApplicationCertificate(this, `cdn_certificate`, { + zoneId: this.baseDNS.zoneId, + domain: this.config.domain, + tags: this.config.tags, + provider: this.config.provider, + }); + + //Create the CDN + const cdn = new CloudfrontDistribution(this, `cloudfront_distribution`, { + comment: `CDN for direct.${this.config.domain}`, + enabled: true, + aliases: [this.config.domain], + priceClass: 'PriceClass_200', + tags: this.config.tags, + origin: [ + { + domainName: albRecord.fqdn, + originId: 'Alb', + customOriginConfig: { + httpPort: 80, + httpsPort: 443, + originProtocolPolicy: 'https-only', + originSslProtocols: ['TLSv1.1', 'TLSv1.2'], + }, + }, + ], + defaultCacheBehavior: { + targetOriginId: 'Alb', + viewerProtocolPolicy: 'redirect-to-https', + compress: true, + allowedMethods: [ + 'GET', + 'HEAD', + 'OPTIONS', + 'PUT', + 'POST', + 'PATCH', + 'DELETE', + ], + cachedMethods: ['GET', 'HEAD', 'OPTIONS'], + forwardedValues: { + queryString: true, + headers: ['Accept', 'Origin', 'Authorization'], //This is important for apollo because it serves different responses based on this + cookies: { + forward: 'none', + }, + }, + //These are hacks to enable Use Origin Cache Header + //https://github.com/hashicorp/terraform-provider-aws/issues/19382 + defaultTtl: 0, //This breaks from the hack, because the default was 0 before. As long as clients specify a cache header this is overridden. + minTtl: 0, + maxTtl: 31536000, // 1 year + }, + viewerCertificate: { + acmCertificateArn: cdnCertificate.arn, + sslSupportMethod: 'sni-only', + minimumProtocolVersion: 'TLSv1.1_2016', + }, + restrictions: { + geoRestriction: { + restrictionType: 'none', + }, + }, + provider: this.config.provider, + }); + + // These are hacks to enable Use Origin Cache Header + // https://github.com/hashicorp/terraform-provider-aws/issues/19382 + cdn.addOverride('default_cache_behavior.default_ttl', 0); + cdn.addOverride('default_cache_behavior.min_ttl', 0); + + //When cached the CDN must point to the Load Balancer + new Route53Record(this, `cdn_record`, { + name: this.config.domain, + type: 'A', + zoneId: this.baseDNS.zoneId, + weightedRoutingPolicy: { + weight: 1, + }, + alias: { + name: cdn.domainName, + zoneId: cdn.hostedZoneId, + evaluateTargetHealth: true, + }, + lifecycle: { + ignoreChanges: ['weighted_routing_policy[0].weight'], + }, + setIdentifier: '2', + provider: this.config.provider, + }); + } + + /** + * Create the ECS service and attach it to the ALB + * @param alb + * @param albCertificate + * @private + */ + private createECSService( + alb: ApplicationLoadBalancer, + albCertificate: ApplicationCertificate, + ): { ecs: ApplicationECSService; cluster: ApplicationECSCluster } { + const ecsCluster = new ApplicationECSCluster(this, 'ecs_cluster', { + prefix: this.config.prefix, + tags: this.config.tags, + }); + + const httpListener = new AlbListener(this, 'listener_http', { + loadBalancerArn: alb.alb.arn, + port: 80, + protocol: 'HTTP', + defaultAction: [ + { + type: 'redirect', + redirect: { port: '443', protocol: 'HTTPS', statusCode: 'HTTP_301' }, + }, + ], + provider: this.config.provider, + tags: this.config.tags, + }); + + const httpsListener = new AlbListener(this, 'listener_https', { + loadBalancerArn: alb.alb.arn, + port: 443, + protocol: 'HTTPS', + sslPolicy: 'ELBSecurityPolicy-TLS-1-1-2017-01', + defaultAction: [ + { + type: 'fixed-response', + fixedResponse: { + // To keep things dry we use a default status code here and append a rule for our target group. + // This is because our ECSService is responsible for creating our target group because CodeDeploy requires + // 2 target groups and knowing the names of them both + contentType: 'text/plain', + statusCode: '503', + messageBody: '', + }, + }, + ], + certificateArn: albCertificate.arn, + provider: this.config.provider, + tags: this.config.tags, + }); + + // We want to be able to make resource changes on the ALB's listeners, so we expose them + this.listeners.push(httpListener, httpsListener); + + let ecsConfig: ApplicationECSServiceProps = { + prefix: this.config.prefix, + region: this.config.region, + shortName: this.config.alb6CharacterPrefix, + ecsClusterArn: ecsCluster.cluster.arn, + ecsClusterName: ecsCluster.cluster.name, + useCodeDeploy: this.config.codeDeploy.useCodeDeploy, + codeDeployNotifications: this.config.codeDeploy.notifications, + useCodePipeline: this.config.codeDeploy.useCodePipeline, + useTerraformBasedCodeDeploy: + this.config.codeDeploy.useTerraformBasedCodeDeploy, + successTerminationWaitTimeInMinutes: + this.config.codeDeploy.successTerminationWaitTimeInMinutes, + codeDeploySnsNotificationTopicArn: + this.config.codeDeploy.snsNotificationTopicArn, + albConfig: { + containerPort: this.config.exposedContainer.port, + containerName: this.config.exposedContainer.name, + healthCheckPath: this.config.exposedContainer.healthCheckPath, + listenerArn: httpsListener.arn, + albSecurityGroupId: alb.securityGroup.id, + }, + vpcId: this.pocketVPC.vpcId, + containerConfigs: this.config.containerConfigs, + privateSubnetIds: this.pocketVPC.privateSubnetIds, + ecsIamConfig: this.config.ecsIamConfig, + provider: this.config.provider, + tags: this.config.tags, + }; + + if (this.config.taskSize) { + ecsConfig = { + ...this.config.taskSize, + ...ecsConfig, + }; + } + + if (this.config.efsConfig) { + ecsConfig.efsConfig = { + efs: { id: this.efs.id, arn: this.efs.arn }, + volumeName: this.config.efsConfig.volumeName, + }; + } + + const ecsService = new ApplicationECSService( + this, + 'ecs_service', + ecsConfig, + ); + + new ApplicationAutoscaling(this, 'autoscaling', { + prefix: this.config.prefix, + targetMinCapacity: this.config.autoscalingConfig.targetMinCapacity, + targetMaxCapacity: this.config.autoscalingConfig.targetMaxCapacity, + ecsClusterName: ecsCluster.cluster.name, + ecsServiceName: ecsService.service.name, + scalableDimension: 'ecs:service:DesiredCount', + stepScaleInAdjustment: + this.config.autoscalingConfig.stepScaleInAdjustment, + stepScaleOutAdjustment: + this.config.autoscalingConfig.stepScaleOutAdjustment, + scaleInThreshold: this.config.autoscalingConfig.scaleInThreshold, + scaleOutThreshold: this.config.autoscalingConfig.scaleOutThreshold, + provider: this.config.provider, + tags: this.config.tags, + }); + + return { + ecs: ecsService, + cluster: ecsCluster, + }; + } + + /** + * Create a Cloudwatch dashboard JSON object + * + * @param albArnSuffix + * @param ecsServiceName + * @param ecsServiceClusterName + */ + private createCloudwatchDashboard( + albArnSuffix: string, + ecsServiceName: string, + ecsServiceClusterName: string, + ): CloudwatchDashboard { + // set some defaults, then ignore all future changes / manual edits & additions. + const dashboardJSON = { + widgets: [ + { + type: 'metric', + x: 0, + y: 0, + width: 12, + height: 6, + properties: { + metrics: [ + [ + 'AWS/ApplicationELB', + 'HTTPCode_Target_4XX_Count', + 'LoadBalancer', + albArnSuffix, + { + yAxis: 'left', + color: '#ff7f0e', + }, + ], + [ + '.', + 'RequestCount', + '.', + '.', + { + yAxis: 'right', + color: '#1f77b4', + }, + ], + [ + '.', + 'HTTPCode_Target_5XX_Count', + '.', + '.', + { + color: '#d62728', + }, + ], + [ + '.', + 'HTTPCode_Target_2XX_Count', + '.', + '.', + { + yAxis: 'right', + color: '#2ca02c', + }, + ], + ], + view: 'timeSeries', + stacked: false, + region: this.config.region ?? 'us-east-1', + period: 60, + stat: 'Sum', + title: 'Target Requests', + }, + }, + { + type: 'metric', + x: 12, + y: 0, + width: 12, + height: 6, + properties: { + metrics: [ + [ + 'AWS/ApplicationELB', + 'HTTPCode_ELB_4XX_Count', + 'LoadBalancer', + albArnSuffix, + { + yAxis: 'left', + color: '#ff7f0e', + }, + ], + [ + '.', + 'RequestCount', + '.', + '.', + { + yAxis: 'right', + color: '#1f77b4', + }, + ], + [ + '.', + 'HTTPCode_ELB_5XX_Count', + '.', + '.', + { + color: '#d62728', + }, + ], + ], + view: 'timeSeries', + stacked: false, + region: this.config.region ?? 'us-east-1', + period: 60, + stat: 'Sum', + title: 'ALB Requests', + }, + }, + { + type: 'metric', + x: 12, + y: 6, + width: 12, + height: 6, + properties: { + metrics: [ + [ + 'AWS/ApplicationELB', + 'TargetResponseTime', + 'LoadBalancer', + albArnSuffix, + { + label: 'Average', + color: '#aec7e8', + }, + ], + [ + '...', + { + stat: 'p95', + label: 'p95', + color: '#ffbb78', + }, + ], + [ + '...', + { + stat: 'p99', + label: 'p99', + color: '#98df8a', + }, + ], + ], + view: 'timeSeries', + stacked: false, + region: this.config.region ?? 'us-east-1', + stat: 'Average', + period: 60, + }, + }, + { + type: 'metric', + x: 0, + y: 6, + width: 12, + height: 6, + properties: { + metrics: [ + [ + 'ECS/ContainerInsights', + 'RunningTaskCount', + 'ServiceName', + ecsServiceName, + 'ClusterName', + ecsServiceClusterName, + { + yAxis: 'right', + color: '#c49c94', + }, + ], + [ + 'AWS/ECS', + 'CPUUtilization', + '.', + '.', + '.', + '.', + { + color: '#f7b6d2', + }, + ], + [ + '.', + 'MemoryUtilization', + '.', + '.', + '.', + '.', + { + color: '#c7c7c7', + }, + ], + ], + view: 'timeSeries', + stacked: false, + region: this.config.region ?? 'us-east-1', + stat: 'Average', + period: 60, + annotations: { + horizontal: [ + { + color: '#e377c2', + label: 'CPU scale out', + value: this.config.autoscalingConfig.scaleOutThreshold, + }, + { + color: '#c5b0d5', + label: 'CPU scale in', + value: this.config.autoscalingConfig.scaleInThreshold, + }, + ], + }, + title: 'Service Load', + }, + }, + ], + }; + + return new CloudwatchDashboard(this, 'cloudwatch-dashboard', { + dashboardName: `${this.config.prefix}`, + dashboardBody: JSON.stringify(dashboardJSON), + lifecycle: { + ignoreChanges: ['dashboard_body'], + }, + provider: this.config.provider, + }); + } + + private createCloudwatchAlarms(): void { + const alarmsConfig = this.config.alarms; + const evaluationPeriods = { + http5xxErrorPercentage: + alarmsConfig?.http5xxErrorPercentage?.evaluationPeriods ?? 5, + httpLatency: alarmsConfig?.httpLatency?.evaluationPeriods ?? 1, + }; + const http5xxAlarm: CloudwatchMetricAlarmConfig = { + alarmName: 'Alarm-HTTPTarget5xxErrorRate', + metricQuery: [ + { + id: 'requests', + metric: { + metricName: 'RequestCount', + namespace: 'AWS/ApplicationELB', + period: alarmsConfig?.http5xxErrorPercentage?.period ?? 60, + stat: 'Sum', + unit: 'Count', + dimensions: { LoadBalancer: this.alb.alb.arnSuffix }, + }, + }, + { + id: 'errors', + metric: { + metricName: 'HTTPCode_Target_5XX_Count', + namespace: 'AWS/ApplicationELB', + period: alarmsConfig?.http5xxErrorPercentage?.period ?? 60, + stat: 'Sum', + unit: 'Count', + dimensions: { LoadBalancer: this.alb.alb.arnSuffix }, + }, + }, + { + id: 'expression', + expression: 'errors/requests*100', + label: 'HTTP 5xx Error Rate', + returnData: true, + }, + ], + comparisonOperator: 'GreaterThanOrEqualToThreshold', + evaluationPeriods: evaluationPeriods.http5xxErrorPercentage, + datapointsToAlarm: + alarmsConfig?.http5xxErrorPercentage?.datapointsToAlarm ?? + evaluationPeriods.http5xxErrorPercentage, + threshold: alarmsConfig?.http5xxErrorPercentage?.threshold ?? 5, + insufficientDataActions: [], + alarmActions: alarmsConfig?.http5xxErrorPercentage?.actions ?? [], + okActions: alarmsConfig?.http5xxErrorPercentage?.actions ?? [], + tags: this.config.tags, + alarmDescription: + alarmsConfig?.http5xxErrorPercentage?.alarmDescription ?? + 'Percentage of 5xx responses exceeds threshold', + provider: this.config.provider, + }; + const latencyAlarm: CloudwatchMetricAlarmConfig = { + alarmName: 'Alarm-HTTPResponseTime', + namespace: 'AWS/ApplicationELB', + metricName: 'TargetResponseTime', + dimensions: { LoadBalancer: this.alb.alb.arnSuffix }, + period: alarmsConfig?.httpLatency?.period ?? 300, + evaluationPeriods: evaluationPeriods.httpLatency, + datapointsToAlarm: + alarmsConfig?.httpLatency?.datapointsToAlarm ?? + evaluationPeriods.httpLatency, + statistic: 'Average', + comparisonOperator: 'GreaterThanThreshold', + threshold: alarmsConfig?.httpLatency?.threshold ?? 300, + alarmDescription: + alarmsConfig?.httpLatency?.alarmDescription ?? + 'Average HTTP response time exceeds threshold', + insufficientDataActions: [], + alarmActions: alarmsConfig?.httpLatency?.actions ?? [], + okActions: alarmsConfig?.httpLatency?.actions ?? [], + provider: this.config.provider, + tags: this.config.tags, + }; + + const defaultAlarms: CloudwatchMetricAlarmConfig[] = []; + + if (alarmsConfig?.http5xxErrorPercentage) defaultAlarms.push(http5xxAlarm); + + if (alarmsConfig?.httpLatency) defaultAlarms.push(latencyAlarm); + + if (alarmsConfig?.customAlarms) { + defaultAlarms.push(...alarmsConfig.customAlarms); + } + + if (defaultAlarms.length) this.createAlarms(defaultAlarms); + } + + private createAlarms(alarms: CloudwatchMetricAlarmConfig[]): void { + alarms.forEach((alarmConfig) => { + new CloudwatchMetricAlarm(this, alarmConfig.alarmName.toLowerCase(), { + ...alarmConfig, + alarmName: `${this.config.prefix}-${alarmConfig.alarmName}`, + }); + }); + } +} diff --git a/packages/terraform-modules/src/pocket/PocketApiGatewayLambdaIntegration.spec.ts b/packages/terraform-modules/src/pocket/PocketApiGatewayLambdaIntegration.spec.ts new file mode 100644 index 00000000..2ff57c8a --- /dev/null +++ b/packages/terraform-modules/src/pocket/PocketApiGatewayLambdaIntegration.spec.ts @@ -0,0 +1,48 @@ +import { Testing } from 'cdktf'; +import { LAMBDA_RUNTIMES } from '../base/ApplicationVersionedLambda'; +import { + PocketApiGateway, + PocketApiGatewayProps, +} from './PocketApiGatewayLambdaIntegration'; + +const config: PocketApiGatewayProps = { + name: 'test-api-lambda', + stage: 'test', + domain: 'exampleapi.getpocket.dev', + basePath: 'fxaProxy', + routes: [ + { + path: 'endpoint', + method: 'POST', + eventHandler: { + name: 'lambda-endpoint', + lambda: { + runtime: LAMBDA_RUNTIMES.PYTHON38, + handler: 'index.handler', + }, + }, + }, + ], +}; + +describe('PocketApiGatewayLambdaIntegration', () => { + const now = 1637693316456; + + beforeAll(() => { + jest.useFakeTimers({ + now: now, + advanceTimers: false, + }); + }); + + afterAll(() => jest.useRealTimers()); + + it('renders an api gateway with a lambda integration', () => { + const synthed = Testing.synthScope((stack) => { + new PocketApiGateway(stack, 'test-api-lambda', { + ...config, + }); + }); + expect(synthed).toMatchSnapshot(); + }); +}); diff --git a/packages/terraform-modules/src/pocket/PocketApiGatewayLambdaIntegration.ts b/packages/terraform-modules/src/pocket/PocketApiGatewayLambdaIntegration.ts new file mode 100644 index 00000000..ad2cc857 --- /dev/null +++ b/packages/terraform-modules/src/pocket/PocketApiGatewayLambdaIntegration.ts @@ -0,0 +1,245 @@ +import { Construct } from 'constructs'; + +import { + PocketVersionedLambda, + PocketVersionedLambdaProps, + ApplicationBaseDNS, + ApplicationCertificate, +} from '..'; +import { Fn, TerraformMetaArguments } from 'cdktf'; +import { ApiGatewayBasePathMapping } from '@cdktf/provider-aws/lib/api-gateway-base-path-mapping'; +import { + ApiGatewayDeploymentConfig, + ApiGatewayDeployment, +} from '@cdktf/provider-aws/lib/api-gateway-deployment'; +import { ApiGatewayDomainName } from '@cdktf/provider-aws/lib/api-gateway-domain-name'; +import { ApiGatewayIntegration } from '@cdktf/provider-aws/lib/api-gateway-integration'; +import { ApiGatewayMethod } from '@cdktf/provider-aws/lib/api-gateway-method'; +import { ApiGatewayResource } from '@cdktf/provider-aws/lib/api-gateway-resource'; +import { ApiGatewayRestApi } from '@cdktf/provider-aws/lib/api-gateway-rest-api'; +import { ApiGatewayStage } from '@cdktf/provider-aws/lib/api-gateway-stage'; +import { LambdaPermission } from '@cdktf/provider-aws/lib/lambda-permission'; +import { Route53Record } from '@cdktf/provider-aws/lib/route53-record'; + +export interface ApiGatewayLambdaRoute { + path: string; + // The HTTP method clients use to call this method (GET, POST, etc.) + method: string; + // Currently not supporting authorizer + // authorizationType: string; + eventHandler: PocketVersionedLambdaProps; + // TODO + // requestParams: Map; +} + +export interface PocketApiGatewayProps extends TerraformMetaArguments { + name: string; + stage: string; // https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/api_gateway_stage#stage_name + routes: ApiGatewayLambdaRoute[]; + tags?: { [key: string]: string }; + // Should not contain lists/arrays, should only contain objects + triggers?: ApiGatewayDeploymentConfig['triggers']; + domain?: string; + basePath?: string; +} + +interface InitializedGatewayRoute { + lambda: PocketVersionedLambda; + resource: ApiGatewayResource; + method: ApiGatewayMethod; + integration: ApiGatewayIntegration; +} + +/** + * Create an API Gateway with lambda handlers, optionally assigned + * to a custom domain owned by the account. + */ +export class PocketApiGateway extends Construct { + private apiGatewayRestApi: ApiGatewayRestApi; + private routes: InitializedGatewayRoute[]; + private apiGatewayDeployment: ApiGatewayDeployment; + private apiGatewayStage: ApiGatewayStage; + + constructor( + scope: Construct, + name: string, + private readonly config: PocketApiGatewayProps, + ) { + super(scope, name); + this.apiGatewayRestApi = new ApiGatewayRestApi(scope, `api-gateway-rest`, { + name: config.name, + tags: config.tags, + provider: config.provider, + }); + + // https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/api_gateway_deployment + this.routes = this.createLambdaIntegrations(config); + const routeDependencies = this.routes.flatMap((route) => [ + route.integration, + route.method, + route.resource, + route.lambda.lambda.versionedLambda, + ]); + + // Use the current timestamp in milliseconds to trigger a redeployment if no triggers are provided. + // If no triggers are provided, this resource will be redeployed on every "terraform apply". + const triggers = config.triggers ?? { deployedAt: Date.now().toString() }; + + // Deployment before adding permissions, so we can restrict to the stage + this.apiGatewayDeployment = new ApiGatewayDeployment( + scope, + 'api-gateway-deployment', + { + restApiId: this.apiGatewayRestApi.id, + triggers: { + redeployment: Fn.sha1( + Fn.jsonencode({ + resources: routeDependencies.map((d) => d.id), + }), + ), + ...triggers, + }, + lifecycle: { createBeforeDestroy: true }, + dependsOn: routeDependencies, + provider: config.provider, + }, + ); + this.apiGatewayStage = new ApiGatewayStage(scope, 'api-gateway-stage', { + deploymentId: this.apiGatewayDeployment.id, + restApiId: this.apiGatewayRestApi.id, + stageName: config.stage, + provider: config.provider, + tags: config.tags, + }); + if (config.domain != null) { + this.createRoute53Record(config); + } + this.addInvokePermissions(); + } + + /** + * Sets up a custom domain name for the API gateway; + * adds ACM Certificate and configures Route 53 Record + * @param config + */ + private createRoute53Record(config: PocketApiGatewayProps) { + //Set up the Base DNS stack for our application which includes a hosted SubZone + const baseDNS = new ApplicationBaseDNS(this, `base-dns`, { + domain: config.domain, + tags: config.tags, + provider: config.provider, + }); + + //Creates the Certificate for API gateway + const apiGatewayCertificate = new ApplicationCertificate( + this, + `api-gateway-certificate`, + { + zoneId: baseDNS.zoneId, + domain: config.domain, + tags: this.config.tags, + provider: config.provider, + }, + ); + + //setup custom domain name for API gateway + const customDomainName = new ApiGatewayDomainName( + this, + `api-gateway-domain-name`, + { + domainName: config.domain, + certificateArn: apiGatewayCertificate.arn, + dependsOn: [apiGatewayCertificate.certificateValidation], + provider: config.provider, + tags: config.tags, + }, + ); + + new Route53Record(this, `apigateway-route53-domain-record`, { + name: customDomainName.domainName, + type: 'A', + zoneId: baseDNS.zoneId, + alias: { + evaluateTargetHealth: true, + name: customDomainName.cloudfrontDomainName, + zoneId: customDomainName.cloudfrontZoneId, + }, + dependsOn: [apiGatewayCertificate.certificateValidation], + provider: config.provider, + }); + + new ApiGatewayBasePathMapping(this, `api-gateway-base-path-mapping`, { + apiId: this.apiGatewayRestApi.id, + stageName: this.apiGatewayStage.stageName, + domainName: customDomainName.domainName, + basePath: config.basePath ?? '', + provider: config.provider, + }); + } + + /** + * Add permissions for gateway to invoke lambda functions + */ + private addInvokePermissions() { + this.routes.map(({ lambda, resource, method }) => { + const friendlyUniqueId = lambda.lambda.versionedLambda.friendlyUniqueId; + new LambdaPermission( + this, + `${friendlyUniqueId}-allow-gateway-lambda-invoke`, + { + functionName: lambda.lambda.versionedLambda.functionName, + action: 'lambda:InvokeFunction', + principal: 'apigateway.amazonaws.com', + // Grants access to invoke lambda on specified stage, method, resource path + // note the resource path has a leading `/` + sourceArn: `${this.apiGatewayRestApi.executionArn}/${this.apiGatewayStage.stageName}/${method.httpMethod}${resource.path}`, + qualifier: lambda.lambda.versionedLambda.name, + provider: this.config.provider, + }, + ); + }); + } + + /** + * Generate aws proxy routes handled by aws lambda + * loops over routes and generate methods + * @param config + */ + private createLambdaIntegrations(config: PocketApiGatewayProps) { + return config.routes.map((route: ApiGatewayLambdaRoute) => { + const lambda = new PocketVersionedLambda( + this, + `${route.path}-lambda`, + route.eventHandler, + ); + const resource = new ApiGatewayResource(this, route.path, { + parentId: this.apiGatewayRestApi.rootResourceId, + pathPart: route.path, + restApiId: this.apiGatewayRestApi.id, + provider: config.provider, + }); + const method = new ApiGatewayMethod(this, `${route.path}-method`, { + restApiId: this.apiGatewayRestApi.id, + resourceId: resource.id, + // authorization: route.authorizationType, + authorization: 'NONE', + httpMethod: route.method, + provider: config.provider, + }); + const integration = new ApiGatewayIntegration( + this, + `${route.path}-integration`, + { + httpMethod: route.method, + integrationHttpMethod: 'POST', // lambda has to be post + resourceId: resource.id, + restApiId: this.apiGatewayRestApi.id, + type: 'AWS_PROXY', + uri: lambda.lambda.versionedLambda.invokeArn, + provider: config.provider, + }, + ); + return { lambda, resource, method, integration }; + }); + } +} diff --git a/packages/terraform-modules/src/pocket/PocketCloudwatchSynthetics.spec.ts b/packages/terraform-modules/src/pocket/PocketCloudwatchSynthetics.spec.ts new file mode 100644 index 00000000..91f5ec00 --- /dev/null +++ b/packages/terraform-modules/src/pocket/PocketCloudwatchSynthetics.spec.ts @@ -0,0 +1,47 @@ +import { Testing } from 'cdktf'; +import { + PocketAwsSyntheticCheckProps, + PocketAwsSyntheticChecks, +} from './PocketCloudwatchSynthetics'; + +const testConfig: PocketAwsSyntheticCheckProps = { + environment: 'Dev', + prefix: 'ACME-Dev', + query: [ + { + endpoint: 'acme.getpocket.dev', + userId: '1', + data: '{"query": "query { someGraphQlQuery(arg1: \\"1\\", arg2: \\"1\\") {returnedAttr} }"}', + jmespath: 'errors[0].message', + response: 'Error - Not Found: A resource by that arg1 could not be found', + }, + ], + shortName: 'ACME', + uptime: [ + { + response: 'ok', + url: `acme.getpocket.dev/.well-known/apollo/server-health`, + }, + ], +}; + +describe('Pocket Cloudwatch Synthetics', () => { + it('renders desired AWS Synthetic Checks', () => { + const synthedStack = Testing.synthScope((stack) => { + new PocketAwsSyntheticChecks(stack, 'test-synthetics', testConfig); + }); + + expect(synthedStack).toMatchSnapshot(); + }); + + it('adds optional Alarms & Alarm Actions to Synthetic Checks ', () => { + testConfig.alarmTopicArn = + 'arn:aws:sns:us-east-1:123456789101:Test-Sns-Topic'; + + const synthedStack = Testing.synthScope((stack) => { + new PocketAwsSyntheticChecks(stack, 'test-synthetics', testConfig); + }); + + expect(synthedStack).toMatchSnapshot(); + }); +}); diff --git a/packages/terraform-modules/src/pocket/PocketCloudwatchSynthetics.ts b/packages/terraform-modules/src/pocket/PocketCloudwatchSynthetics.ts new file mode 100644 index 00000000..fb2d2a4a --- /dev/null +++ b/packages/terraform-modules/src/pocket/PocketCloudwatchSynthetics.ts @@ -0,0 +1,333 @@ +// Use this module over PocketSynthetics.ts when checks need to be inside a Pocket VPC. +import { CloudwatchMetricAlarm } from '@cdktf/provider-aws/lib/cloudwatch-metric-alarm'; +import { DataAwsIamPolicyDocument } from '@cdktf/provider-aws/lib/data-aws-iam-policy-document'; +import { IamPolicy } from '@cdktf/provider-aws/lib/iam-policy'; +import { IamRole } from '@cdktf/provider-aws/lib/iam-role'; +import { IamRolePolicyAttachment } from '@cdktf/provider-aws/lib/iam-role-policy-attachment'; +import { S3Bucket } from '@cdktf/provider-aws/lib/s3-bucket'; +import { S3BucketLifecycleConfiguration } from '@cdktf/provider-aws/lib/s3-bucket-lifecycle-configuration'; +import { SyntheticsCanary } from '@cdktf/provider-aws/lib/synthetics-canary'; +import { Construct } from 'constructs'; + +/** + * + * Query Configs are used for Synthetics to make POST requests, + * then check the expected response, optionally at a JMESPath node + * in the response body. (see https://jmespath.org/ if unfamililar). + * Built for GraphQL operation checks, but can be used for any POST. + * + */ +interface PocketAwsSyntheticQueryConfig { + data?: string; + userId?: string; + endpoint?: string; + jmespath?: string; + response?: string; +} + +/** + * + * Uptime Configs are used for Synthetics to make GET requests, + * then check the response code is 2XX, + * & optionally check an expected response body (no parsing). + * + */ +interface PocketAwsSyntheticUptimeConfig { + response?: string; + url?: string; +} + +/** + * + * This is the config interface for both uptime & query checks (see above). + * Arrays of each can be passed in (or not), for as many endpoints to check + * as desired. TBD: make each endpoint differently configurable for alerting + * (all checks using this module will alert via 1 shared action currently). + * + */ +export interface PocketAwsSyntheticCheckProps { + alarmTopicArn?: string | null; + environment: string; // reusable code setup only exists in AWS Dev & Prod Accounts currently. + prefix: string; + query: PocketAwsSyntheticQueryConfig[]; + securityGroupIds?: string[]; + shortName: string; + subnetIds?: string[]; + tags?: { [key: string]: string }; + uptime: PocketAwsSyntheticUptimeConfig[]; +} + +/** + * + * Create AWS Cloudwatch Synthetics with some Uptime & Query check baselines + * + */ +export class PocketAwsSyntheticChecks extends Construct { + constructor( + scope: Construct, + private name: string, + private config: PocketAwsSyntheticCheckProps, + ) { + super(scope, name); + + // synthetic response artifacts are stored here + const syntheticArtifactsS3 = new S3Bucket( + this, + `${this.name}_synthetic_check_artifacts`, + { + bucket: `pocket-${this.config.prefix.toLowerCase()}-synthetic-checks`, + }, + ); + + new S3BucketLifecycleConfiguration( + this, + `${this.name}_synthetic_check_artifacts_lifecycle`, + { + bucket: syntheticArtifactsS3.id, + rule: [ + { + expiration: { + days: 30, + }, + id: '30-day-retention', + status: 'Enabled', + }, + ], + }, + ); + + // behind the scenes, Cloudwatch Synthetics are AWS-managed Lambdas + const dataSyntheticAssume = new DataAwsIamPolicyDocument( + this, + `${this.name}_synthetic_check_assume`, + { + version: '2012-10-17', + statement: [ + { + effect: 'Allow', + actions: ['sts:AssumeRole'], + + principals: [ + { + identifiers: ['lambda.amazonaws.com'], + type: 'Service', + }, + ], + }, + ], + }, + ); + + const syntheticRole = new IamRole(this, 'synthetic_check_role', { + name: `pocket-${this.config.prefix.toLowerCase()}-synthetic-check`, + + assumeRolePolicy: dataSyntheticAssume.json, + tags: this.config.tags, + }); + + // puts artifacts into s3, stores logs, pushes metrics to Cloudwatch + // also create networkinterfaces if synthetic check in VPC + const dataSyntheticAccess = new DataAwsIamPolicyDocument( + this, + `${this.name}_synthetic_check_access`, + { + version: '2012-10-17', + statement: [ + { + effect: 'Allow', + actions: [ + 'logs:CreateLogGroup', + 'logs:CreateLogStream', + 'logs:PutLogEvents', + ], + resources: ['*'], + }, + { + actions: ['s3:PutObject', 's3:GetObject'], + resources: [`${syntheticArtifactsS3.arn}/*`], + }, + { + actions: ['s3:GetObject'], + resources: [ + `arn:aws:s3:::pocket-syntheticchecks-${this.config.environment.toLowerCase()}/*`, + ], + }, + { + actions: ['s3:GetBucketLocation'], + resources: [syntheticArtifactsS3.arn], + }, + { + actions: ['s3:ListAllMyBuckets'], + resources: ['*'], + }, + { + actions: ['cloudwatch:PutMetricData'], + resources: ['*'], + condition: [ + { + test: 'StringEquals', + values: ['CloudWatchSynthetics'], + variable: 'cloudwatch:namespace', + }, + ], + }, + { + actions: [ + 'ec2:AttachNetworkInterface', + 'ec2:CreateNetworkInterface', + 'ec2:DeleteNetworkInterface', + 'ec2:DescribeNetworkInterfaces', + ], + resources: ['*'], + }, + ], + }, + ); + + const syntheticAccessPolicy = new IamPolicy( + this, + `${this.name}_synthetic_check_access_policy`, + { + name: `pocket-${this.config.prefix.toLowerCase()}-synthetic-check-access`, + policy: dataSyntheticAccess.json, + }, + ); + + new IamRolePolicyAttachment( + this, + `${this.name}_synthetic_check_access_attach`, + { + role: syntheticRole.id, + policyArn: syntheticAccessPolicy.arn, + }, + ); + + for (const uptimeConfig of this.config.uptime) { + const count = this.config.uptime.indexOf(uptimeConfig); + const check = new SyntheticsCanary( + this, + `${this.name}_synthetic_check_uptime_${count}`, + { + name: `${this.config.shortName.toLowerCase()}-${this.config.environment.toLowerCase()}-uptime-${count}`, // limit of 21 characters + artifactS3Location: `s3://${syntheticArtifactsS3.bucket}/`, + executionRoleArn: syntheticRole.arn, + handler: 'synthetic.uptime', + runConfig: { + environmentVariables: { + UPTIME_BODY: uptimeConfig.response, + UPTIME_URL: uptimeConfig.url, + }, + timeoutInSeconds: 180, // 3 minute timeout + }, + runtimeVersion: 'syn-nodejs-puppeteer-4.0', + s3Bucket: `pocket-syntheticchecks-${this.config.environment.toLowerCase()}`, + s3Key: `aws-synthetic-${this.config.environment.toLowerCase()}.zip`, + schedule: { + expression: 'rate(5 minutes)', // run every 5 minutes + }, + startCanary: true, + vpcConfig: { + subnetIds: this.config.subnetIds, + securityGroupIds: this.config.securityGroupIds, + }, + }, + ); + + new CloudwatchMetricAlarm( + this, + `${this.name}_synthetic_check_alarm_uptime_${count}`, + { + alarmDescription: `Alert when ${check.name} canary success percentage has decreased below 66% in the last 15 minutes`, + alarmName: check.name, + comparisonOperator: 'LessThanThreshold', + dimensions: { + CanaryName: check.name, + }, + evaluationPeriods: 3, + metricName: 'SuccessPercent', + namespace: 'CloudWatchSynthetics', + period: 300, // 15 minutes + statistic: 'Average', + threshold: 66, + treatMissingData: 'breaching', + + alarmActions: + config.alarmTopicArn !== undefined + ? [this.config.alarmTopicArn] + : null, + insufficientDataActions: [], + okActions: + config.alarmTopicArn !== undefined + ? [this.config.alarmTopicArn] + : null, + }, + ); + } + + for (const queryConfig of this.config.query) { + const count = this.config.query.indexOf(queryConfig); + const check = new SyntheticsCanary( + this, + `${this.name}_synthetic_check_query_${count}`, + { + name: `${this.config.shortName.toLowerCase()}-${this.config.environment.toLowerCase()}-query-${count}`, // limit of 21 characters + artifactS3Location: `s3://${syntheticArtifactsS3.bucket}/`, + executionRoleArn: syntheticRole.arn, + handler: 'synthetic.query', + runConfig: { + environmentVariables: { + GRAPHQL_ENDPOINT: queryConfig.endpoint, + GRAPHQL_USERID: queryConfig.userId, + GRAPHQL_JMESPATH: queryConfig.jmespath, + GRAPHQL_QUERY: queryConfig.data, + GRAPHQL_RESPONSE: queryConfig.response, + }, + timeoutInSeconds: 180, // 3 minute timeout + }, + runtimeVersion: 'syn-nodejs-puppeteer-4.0', + s3Bucket: `pocket-syntheticchecks-${this.config.environment.toLowerCase()}`, + s3Key: `aws-synthetic-${this.config.environment.toLowerCase()}.zip`, + schedule: { + expression: 'rate(5 minutes)', // run every 5 minutes + }, + startCanary: true, + vpcConfig: { + subnetIds: this.config.subnetIds, + securityGroupIds: this.config.securityGroupIds, + }, + }, + ); + + new CloudwatchMetricAlarm( + this, + `${this.name}_synthetic_check_alarm_query_${count}`, + { + alarmDescription: `Alert when ${check.name} canary success percentage has decreased below 66% in the last 15 minutes`, + alarmName: check.name, + + comparisonOperator: 'LessThanThreshold', + dimensions: { + CanaryName: check.name, + }, + evaluationPeriods: 3, + metricName: 'SuccessPercent', + namespace: 'CloudWatchSynthetics', + period: 300, // 15 minutes + statistic: 'Average', + threshold: 66, + treatMissingData: 'breaching', + + alarmActions: + config.alarmTopicArn !== undefined + ? [this.config.alarmTopicArn] + : [], + insufficientDataActions: [], + okActions: + config.alarmTopicArn !== undefined + ? [this.config.alarmTopicArn] + : [], + }, + ); + } + } +} diff --git a/packages/terraform-modules/src/pocket/PocketECSApplication.spec.ts b/packages/terraform-modules/src/pocket/PocketECSApplication.spec.ts new file mode 100644 index 00000000..18a8381d --- /dev/null +++ b/packages/terraform-modules/src/pocket/PocketECSApplication.spec.ts @@ -0,0 +1,135 @@ +import { TerraformStack, Testing } from 'cdktf'; +import { + PocketECSApplication, + PocketECSApplicationProps, +} from './PocketECSApplication'; + +describe('PocketECSApplication', () => { + let BASE_CONFIG: PocketECSApplicationProps; + + beforeEach(() => { + BASE_CONFIG = { + prefix: 'testapp', + containerConfigs: [], + ecsIamConfig: { + prefix: 'testapp-', + taskExecutionDefaultAttachmentArn: '', + taskExecutionRolePolicyStatements: [], + taskRolePolicyStatements: [], + }, + }; + }); + + it('renders an application with minimal config', () => { + const synthed = Testing.synthScope((stack) => { + new PocketECSApplication(stack, 'testPocketApp', BASE_CONFIG); + }); + expect(synthed).toMatchSnapshot(); + }); + + it('renders an application with custom task sizes', () => { + const synthed = Testing.synthScope((stack) => { + BASE_CONFIG.taskSize = { + cpu: 8675, + memory: 309, + }; + + new PocketECSApplication(stack, 'testPocketApp', BASE_CONFIG); + }); + expect(synthed).toMatchSnapshot(); + }); + + it('renders an application with autoscaling group and tags', () => { + const synthed = Testing.synthScope((stack) => { + BASE_CONFIG.tags = { + name: 'thedude', + hobby: 'bowling', + }; + + BASE_CONFIG.autoscalingConfig = { + targetMinCapacity: 1, + targetMaxCapacity: 2, + stepScaleInAdjustment: -1, + stepScaleOutAdjustment: 2, + scaleInThreshold: 30, + scaleOutThreshold: 45, + }; + + new PocketECSApplication(stack, 'testPocketApp', BASE_CONFIG); + }); + expect(synthed).toMatchSnapshot(); + }); + + it('renders an application with default autoscaling group and tags', () => { + const synthed = Testing.synthScope((stack) => { + BASE_CONFIG.tags = { + name: 'thedude', + hobby: 'bowling', + }; + + new PocketECSApplication(stack, 'testPocketApp', BASE_CONFIG); + }); + expect(synthed).toMatchSnapshot(); + }); + + it('renders an application with modified container def protocol, cpu and memory reservation', () => { + const synthed = Testing.synthScope((stack) => { + BASE_CONFIG.containerConfigs = [ + { + name: 'xray-daemon', + portMappings: [ + { + hostPort: 0, + containerPort: 2000, + protocol: 'udp', + }, + ], + cpu: 10, + memoryReservation: 50, + }, + ]; + + new PocketECSApplication(stack, 'testPocketApp', BASE_CONFIG); + }); + expect(synthed).toMatchSnapshot(); + }); + + it('validates custom alarms config', () => { + const app = Testing.app(); + const stack = new TerraformStack(app, 'test'); + + const alarmConfig = { + ...BASE_CONFIG, + alarms: { + alarms: [ + { + alarmName: `Test-Alarm-Custom`, + namespace: 'TM/Alarm', + metricName: 'Custom', + dimensions: { Test: 'Alarm' }, + period: 300, + statistic: 'Sum', + comparisonOperator: 'GreaterThanThreshold', + threshold: 500, + evaluationPeriods: 1, + datapointsToAlarm: 2, + }, + ], + }, + }; + + expect( + () => new PocketECSApplication(stack, 'testPocketApp', alarmConfig), + ).toThrow(Error); + }); + + it('renders an Pocket App with logs and dashboard in a specified region', () => { + const synthed = Testing.synthScope((stack) => { + new PocketECSApplication(stack, 'testPocketApp', { + ...BASE_CONFIG, + region: 'central region', + }); + }); + expect(synthed).toMatchSnapshot(); + }); +}); diff --git a/packages/terraform-modules/src/pocket/PocketECSApplication.ts b/packages/terraform-modules/src/pocket/PocketECSApplication.ts new file mode 100644 index 00000000..ef582fa1 --- /dev/null +++ b/packages/terraform-modules/src/pocket/PocketECSApplication.ts @@ -0,0 +1,382 @@ +import { CloudwatchDashboard } from '@cdktf/provider-aws/lib/cloudwatch-dashboard'; +import { + CloudwatchMetricAlarmConfig, + CloudwatchMetricAlarm, +} from '@cdktf/provider-aws/lib/cloudwatch-metric-alarm'; +import { AwsProvider } from '@cdktf/provider-aws/lib/provider'; +import { TerraformMetaArguments } from 'cdktf'; +import { Construct } from 'constructs'; +import { + ApplicationAutoscaling, + ApplicationECSCluster, + ApplicationECSContainerDefinitionProps, + ApplicationECSIAMProps, + ApplicationECSService, + ApplicationECSServiceProps, +} from '../'; +import { PocketVPC } from './PocketVPC'; + +export type CreateECSServiceArgs = { + ecs: ApplicationECSService; + cluster: ApplicationECSCluster; +}; + +export interface PocketECSApplicationProps extends TerraformMetaArguments { + /** + * This is the prefix for the names of all the resources + * created by this construct. + */ + prefix: string; + + shortName?: string; + /** + * Optional config to define the region for the service. + * This is used to define the cloudwatch dashboards + * as well as the region of the cloudwatch logs + */ + region?: string; + /** + * VPC configuration for all resource that require it within + * this construct. A default Pocket VPC will be used if not + * provided. + */ + vpcConfig?: { + vpcId: string; + privateSubnetIds: string[]; + publicSubnetIds: string[]; + }; + /** + * Tags for all resources created by this construct. + */ + tags?: { [key: string]: string }; + /** + * Container definitions for the ECS task. + */ + containerConfigs: ApplicationECSContainerDefinitionProps[]; + /** + * ECS task size configuration. + */ + taskSize?: { + cpu: number; + memory: number; + }; + /** + * IAM config for the ECS Service and Tasks. + */ + ecsIamConfig: ApplicationECSIAMProps; + /** + * Options for configuring the autoscaling policy for + * the ECS service created by this construct. + */ + autoscalingConfig?: { + targetMinCapacity?: number; + targetMaxCapacity?: number; + stepScaleInAdjustment?: number; + stepScaleOutAdjustment?: number; + scaleInThreshold?: number; + scaleOutThreshold?: number; + }; + /** + * Option for defining Cloudwatch alarms + */ + alarms?: { + alarms?: CloudwatchMetricAlarmConfig[]; + }; +} + +// can export if we need to use outside of this module +const DEFAULT_AUTOSCALING_CONFIG = { + scaleOutThreshold: 45, + scaleInThreshold: 30, + targetMinCapacity: 1, + targetMaxCapacity: 2, + stepScaleInAdjustment: -1, + stepScaleOutAdjustment: 2, +}; + +export class PocketECSApplication extends Construct { + public readonly ecsService: ApplicationECSService; + private readonly config: PocketECSApplicationProps; + private readonly pocketVPC: PocketECSApplicationProps['vpcConfig']; + + constructor( + scope: Construct, + name: string, + config: PocketECSApplicationProps, + ) { + super(scope, name); + + this.config = PocketECSApplication.validateConfig(config); + + // use default auto-scaling config, but update any user-provided values + this.config.autoscalingConfig = { + ...DEFAULT_AUTOSCALING_CONFIG, + ...config.autoscalingConfig, + }; + + this.pocketVPC = this.getVpcConfig(config); + + const ecsService = this.createECSService(); + this.ecsService = ecsService.ecs; + + this.createCloudwatchDashboard( + ecsService.ecs.service.name, + ecsService.cluster.cluster.name, + ); + + this.createCloudwatchAlarms(); + } + + /** + * Fall back to standard Pocket config if a custom VPC configuration has not been supplied. + * + * @param config + * @private + */ + private getVpcConfig( + config: PocketECSApplicationProps, + ): PocketECSApplicationProps['vpcConfig'] { + if (config.vpcConfig !== undefined) { + return { + vpcId: config.vpcConfig.vpcId, + privateSubnetIds: config.vpcConfig.privateSubnetIds, + publicSubnetIds: config.vpcConfig.publicSubnetIds, + }; + } else { + const pocketVpc = new PocketVPC( + this, + `pocket_vpc`, + config.provider as AwsProvider, + ); + return { + vpcId: pocketVpc.vpc.id, + privateSubnetIds: pocketVpc.privateSubnetIds, + publicSubnetIds: pocketVpc.publicSubnetIds, + }; + } + } + + /** + * Validate the Configuration + * + * @param config + * @private + */ + private static validateConfig( + config: PocketECSApplicationProps, + ): PocketECSApplicationProps { + PocketECSApplication.validateAlarmsConfig(config.alarms); + + return config; + } + + private static validateAlarmsConfig( + config: PocketECSApplicationProps['alarms'], + ): void { + if (!config) return; + + const alarmsToValidate = { + http5xxErrorPercentage: 'HTTP 5xx Error', + httpLatency: 'HTTP Latency', + httpRequestCount: 'HTTP Request Count', + }; + + const errorMessage = + 'DatapointsToAlarm must be less than or equal to EvaluationPeriods'; + + Object.keys(alarmsToValidate).forEach((key) => { + if ( + config[key]?.datapointsToAlarm > (config[key]?.evaluationPeriods ?? 1) + ) { + throw new Error(`${alarmsToValidate[key]} Alarm: ${errorMessage}`); + } + }); + + config.alarms?.forEach((alarm: CloudwatchMetricAlarmConfig) => { + if (alarm.datapointsToAlarm > alarm.evaluationPeriods) { + throw new Error(`${alarm.alarmName}: ${errorMessage}`); + } + }); + } + + /** + * Create the ECS service + * @private + */ + private createECSService(): CreateECSServiceArgs { + const ecsCluster = new ApplicationECSCluster(this, 'ecs_cluster', { + prefix: this.config.prefix, + tags: this.config.tags, + provider: this.config.provider, + }); + + let ecsConfig: ApplicationECSServiceProps = { + useCodeDeploy: false, + prefix: this.config.prefix, + region: this.config.region, + shortName: this.config.shortName, + ecsClusterArn: ecsCluster.cluster.arn, + ecsClusterName: ecsCluster.cluster.name, + vpcId: this.pocketVPC.vpcId, + containerConfigs: this.config.containerConfigs, + privateSubnetIds: this.pocketVPC.privateSubnetIds, + ecsIamConfig: this.config.ecsIamConfig, + tags: this.config.tags, + provider: this.config.provider, + }; + + if (this.config.taskSize) { + ecsConfig = { + ...this.config.taskSize, + ...ecsConfig, + }; + } + + const ecsService = new ApplicationECSService( + this, + 'ecs_service', + ecsConfig, + ); + + new ApplicationAutoscaling(this, 'autoscaling', { + prefix: this.config.prefix, + targetMinCapacity: this.config.autoscalingConfig.targetMinCapacity, + targetMaxCapacity: this.config.autoscalingConfig.targetMaxCapacity, + ecsClusterName: ecsCluster.cluster.name, + ecsServiceName: ecsService.service.name, + scalableDimension: 'ecs:service:DesiredCount', + stepScaleInAdjustment: + this.config.autoscalingConfig.stepScaleInAdjustment, + stepScaleOutAdjustment: + this.config.autoscalingConfig.stepScaleOutAdjustment, + scaleInThreshold: this.config.autoscalingConfig.scaleInThreshold, + scaleOutThreshold: this.config.autoscalingConfig.scaleOutThreshold, + tags: this.config.tags, + provider: this.config.provider, + }); + + return { + ecs: ecsService, + cluster: ecsCluster, + }; + } + + /** + * Create a Cloudwatch dashboard JSON object + * + * @param ecsServiceName + * @param ecsServiceClusterName + */ + private createCloudwatchDashboard( + ecsServiceName: string, + ecsServiceClusterName: string, + ): CloudwatchDashboard { + // set some defaults, then ignore all future changes / manual edits & additions. + const dashboardJSON = { + widgets: [ + { + type: 'metric', + x: 0, + y: 6, + width: 12, + height: 6, + properties: { + metrics: [ + [ + 'ECS/ContainerInsights', + 'RunningTaskCount', + 'ServiceName', + ecsServiceName, + 'ClusterName', + ecsServiceClusterName, + { + yAxis: 'right', + color: '#c49c94', + }, + ], + [ + 'AWS/ECS', + 'CPUUtilization', + '.', + '.', + '.', + '.', + { + color: '#f7b6d2', + }, + ], + [ + '.', + 'MemoryUtilization', + '.', + '.', + '.', + '.', + { + color: '#c7c7c7', + }, + ], + ], + view: 'timeSeries', + stacked: false, + region: this.config.region ?? 'us-east-1', + stat: 'Average', + period: 60, + annotations: { + horizontal: [ + { + color: '#e377c2', + label: 'CPU scale out', + value: this.config.autoscalingConfig.scaleOutThreshold, + }, + { + color: '#c5b0d5', + label: 'CPU scale in', + value: this.config.autoscalingConfig.scaleInThreshold, + }, + ], + }, + title: 'Service Load', + }, + }, + ], + }; + + return new CloudwatchDashboard(this, 'cloudwatch-dashboard', { + dashboardName: `${this.config.prefix}`, + dashboardBody: JSON.stringify(dashboardJSON), + lifecycle: { + ignoreChanges: ['dashboard_body'], + }, + provider: this.config.provider, + }); + } + + /** + * @private + */ + private createCloudwatchAlarms(): void { + const alarmsConfig = this.config.alarms; + + const defaultAlarms: CloudwatchMetricAlarmConfig[] = []; + + if (alarmsConfig?.alarms) { + defaultAlarms.push(...alarmsConfig.alarms); + } + + if (defaultAlarms.length) this.createAlarms(defaultAlarms); + } + + /** + * @param alarms + * @private + */ + private createAlarms(alarms: CloudwatchMetricAlarmConfig[]): void { + alarms.forEach((alarmConfig) => { + new CloudwatchMetricAlarm(this, alarmConfig.alarmName.toLowerCase(), { + ...alarmConfig, + alarmName: `${this.config.prefix}-${alarmConfig.alarmName}`, + } as CloudwatchMetricAlarmConfig); + }); + } +} diff --git a/packages/terraform-modules/src/pocket/PocketECSCodePipeline.spec.ts b/packages/terraform-modules/src/pocket/PocketECSCodePipeline.spec.ts new file mode 100644 index 00000000..7266671b --- /dev/null +++ b/packages/terraform-modules/src/pocket/PocketECSCodePipeline.spec.ts @@ -0,0 +1,145 @@ +import { Testing } from 'cdktf'; +import { + PocketECSCodePipeline, + PocketECSCodePipelineProps, +} from './PocketECSCodePipeline'; + +const config: PocketECSCodePipelineProps = { + prefix: 'Test-Env', + source: { + repository: 'string', + branchName: 'test', + codeStarConnectionArn: 'arn:codestart-connection:*', + }, +}; + +test('renders a Pocket ECS Codepipeline template', () => { + const synthed = Testing.synthScope((stack) => { + new PocketECSCodePipeline(stack, 'test-codepipeline', config); + }); + expect(synthed).toMatchSnapshot(); +}); + +test('renders a Pocket ECS Codepipeline template with tags', () => { + const synthed = Testing.synthScope((stack) => { + new PocketECSCodePipeline(stack, 'test-codepipeline', { + ...config, + tags: { yup: 'a tag' }, + }); + }); + expect(synthed).toMatchSnapshot(); +}); + +test('renders a Pocket ECS Codepipeline template with the provided code build project name', () => { + const synthed = Testing.synthScope((stack) => { + new PocketECSCodePipeline(stack, 'test-codepipeline', { + ...config, + codeBuildProjectName: 'TestBuildName', + }); + }); + expect(synthed).toMatchSnapshot(); +}); + +test('renders a Pocket ECS Codepipeline template with the provided CodeDeploy app name', () => { + const synthed = Testing.synthScope((stack) => { + new PocketECSCodePipeline(stack, 'test-codepipeline', { + ...config, + codeDeploy: { + applicationName: 'TestAppName', + }, + }); + }); + expect(synthed).toMatchSnapshot(); +}); + +test('renders a Pocket ECS Codepipeline template with the provided CodeDeploy deployment group name', () => { + const synthed = Testing.synthScope((stack) => { + new PocketECSCodePipeline(stack, 'test-codepipeline', { + ...config, + codeDeploy: { + deploymentGroupName: 'TestDeploymentGroupName', + }, + }); + }); + expect(synthed).toMatchSnapshot(); +}); + +test('renders a Pocket ECS Codepipeline template with the provided artifact bucket prefix', () => { + const synthed = Testing.synthScope((stack) => { + new PocketECSCodePipeline(stack, 'test-codepipeline', { + ...config, + artifactBucketPrefix: 'my-codepipeline', + }); + }); + expect(synthed).toMatchSnapshot(); +}); + +test('renders a Pocket ECS Codepipeline template with the provided appspec path', () => { + const synthed = Testing.synthScope((stack) => { + new PocketECSCodePipeline(stack, 'test-codepipeline', { + ...config, + codeDeploy: { + appSpecPath: 'testappspec.json', + }, + }); + }); + expect(synthed).toMatchSnapshot(); +}); + +test('renders a Pocket ECS Codepipeline template with the provided taskdef path', () => { + const synthed = Testing.synthScope((stack) => { + new PocketECSCodePipeline(stack, 'test-codepipeline', { + ...config, + codeDeploy: { + taskDefPath: 'testtaskdef.json', + }, + }); + }); + expect(synthed).toMatchSnapshot(); +}); + +test('renders a Pocket ECS Codepipeline template with custom steps', () => { + const synthed = Testing.synthScope((stack) => { + new PocketECSCodePipeline(stack, 'test-codepipeline', { + ...config, + preDeployStages: [ + { + name: 'Custom_PreDeploy_Stage', + action: [ + { + name: 'MyBuild', + category: 'Deploy', + owner: 'AWS', + provider: 'CodeBuild', + version: '1', + configuration: { + projectName: 'Test-Env-MyBuild', + }, + runOrder: 1, + }, + ], + }, + ], + postDeployStages: [ + { + name: 'Custom_PostDeploy_Stage', + action: [ + { + name: 'Custom_Action', + category: 'Deploy', + owner: 'AWS', + provider: 'StepFunctions', + version: '1', + configuration: { + stateMachineArn: 'myStateMachineArn123', + ExecutionNamePrefix: 'CodePipelineDeploy', + }, + runOrder: 1, + }, + ], + }, + ], + }); + }); + expect(synthed).toMatchSnapshot(); +}); diff --git a/packages/terraform-modules/src/pocket/PocketECSCodePipeline.ts b/packages/terraform-modules/src/pocket/PocketECSCodePipeline.ts new file mode 100644 index 00000000..d0b3e55d --- /dev/null +++ b/packages/terraform-modules/src/pocket/PocketECSCodePipeline.ts @@ -0,0 +1,368 @@ +import { + CodepipelineStage, + Codepipeline, + CodepipelineStageAction, +} from '@cdktf/provider-aws/lib/codepipeline'; +import { DataAwsIamPolicyDocument } from '@cdktf/provider-aws/lib/data-aws-iam-policy-document'; +import { DataAwsKmsAlias } from '@cdktf/provider-aws/lib/data-aws-kms-alias'; +import { IamRole } from '@cdktf/provider-aws/lib/iam-role'; +import { IamRolePolicy } from '@cdktf/provider-aws/lib/iam-role-policy'; +import { S3Bucket } from '@cdktf/provider-aws/lib/s3-bucket'; +import { S3BucketAcl } from '@cdktf/provider-aws/lib/s3-bucket-acl'; +import { S3BucketOwnershipControls } from '@cdktf/provider-aws/lib/s3-bucket-ownership-controls'; +import { TerraformMetaArguments } from 'cdktf'; +import { Construct } from 'constructs'; +import crypto from 'crypto'; + +export interface PocketECSCodePipelineProps extends TerraformMetaArguments { + prefix: string; + artifactBucketPrefix?: string; + source: { + repository: string; + branchName: string; + codeStarConnectionArn: string; + }; + codeBuildProjectName?: string; + codeDeploy?: { + applicationName?: string; + deploymentGroupName?: string; + appSpecPath?: string; + taskDefPath?: string; + }; + /** Optional stages to run before the deploy stage. + * For CodeBuild actions, ensure that the project name starts with `prefix`. + */ + preDeployStages?: CodepipelineStage[]; + /** Optional stages to run after the deploy stage. + * For CodeBuild actions, ensure that the project name starts with `prefix`. + */ + postDeployStages?: CodepipelineStage[]; + tags?: { [key: string]: string }; +} + +export class PocketECSCodePipeline extends Construct { + private static DEFAULT_TASKDEF_PATH = 'taskdef.json'; + private static DEFAULT_APPSPEC_PATH = 'appspec.json'; + + public readonly codePipeline: Codepipeline; + public readonly stages: CodepipelineStage[]; + private readonly pipelineArtifactBucket: S3Bucket; + private readonly s3KmsAlias: DataAwsKmsAlias; + private readonly pipelineRole: IamRole; + + private readonly codeBuildProjectName: string; + private readonly codeDeployApplicationName: string; + private readonly codeDeployDeploymentGroupName: string; + private readonly taskDefinitionTemplatePath: string; + private readonly appSpecTemplatePath: string; + + constructor( + scope: Construct, + name: string, + private config: PocketECSCodePipelineProps, + ) { + super(scope, name); + + this.codeBuildProjectName = this.getCodeBuildProjectName(); + this.codeDeployApplicationName = this.getCodeDeployApplicationName(); + this.codeDeployDeploymentGroupName = + this.getCodeDeployDeploymentGroupName(); + this.taskDefinitionTemplatePath = this.getTaskDefinitionTemplatePath(); + this.appSpecTemplatePath = this.getAppSpecTemplatePath(); + + this.s3KmsAlias = this.createS3KmsAlias(); + this.pipelineArtifactBucket = this.createArtifactBucket(); + this.pipelineRole = this.createPipelineRole(); + this.codePipeline = this.createCodePipeline(); + } + + private getPipelineName = () => `${this.config.prefix}-CodePipeline`; + + private getCodeBuildProjectName = () => + this.config.codeBuildProjectName ?? this.config.prefix; + + private getArtifactBucketPrefix = () => + this.config.artifactBucketPrefix ?? 'pocket-codepipeline'; + + private getCodeDeployApplicationName = () => + this.config.codeDeploy?.applicationName ?? `${this.config.prefix}-ECS`; + + private getCodeDeployDeploymentGroupName = () => + this.config.codeDeploy?.deploymentGroupName ?? `${this.config.prefix}-ECS`; + + private getTaskDefinitionTemplatePath = () => + this.config.codeDeploy?.taskDefPath ?? + PocketECSCodePipeline.DEFAULT_TASKDEF_PATH; + + private getAppSpecTemplatePath = () => + this.config.codeDeploy?.appSpecPath ?? + PocketECSCodePipeline.DEFAULT_APPSPEC_PATH; + + /** + * Get all stages for the pipeline, including postDeployStage if provided. + * @private + */ + private getStages = () => [ + this.getSourceStage(), + ...(this.config.preDeployStages ? this.config.preDeployStages : []), + this.getDeployStage(), + ...(this.config.postDeployStages ? this.config.postDeployStages : []), + ]; + + private createS3KmsAlias() { + return new DataAwsKmsAlias(this, 'kms_s3_alias', { + name: 'alias/aws/s3', + provider: this.config.provider, + }); + } + + /** + * Create a CodePipeline that runs CodeBuild and ECS CodeDeploy + * @private + */ + private createCodePipeline(): Codepipeline { + return new Codepipeline(this, 'codepipeline', { + name: this.getPipelineName(), + roleArn: this.pipelineRole.arn, + artifactStore: this.getArtifactStore(), + stage: this.getStages(), + tags: this.config.tags, + provider: this.config.provider, + }); + } + + /** + * Create CodePipeline artifact s3 bucket + * @private + */ + private createArtifactBucket() { + const prefixHash = crypto + .createHash('md5') + .update(this.config.prefix) + .digest('hex'); + + const s3Bucket = new S3Bucket(this, 'codepipeline-bucket', { + bucket: `${this.getArtifactBucketPrefix()}-${prefixHash}`, + forceDestroy: true, + tags: this.config.tags, + provider: this.config.provider, + }); + + const ownership = new S3BucketOwnershipControls( + this, + 'code-bucket-ownership-controls', + { + bucket: s3Bucket.id, + rule: { + objectOwnership: 'BucketOwnerPreferred', + }, + }, + ); + + new S3BucketAcl(this, 'code-bucket-acl', { + bucket: s3Bucket.id, + acl: 'private', + dependsOn: [ownership], + }); + + return s3Bucket; + } + + /** + * Creates a CodePipeline role. + * @private + */ + private createPipelineRole() { + const role = new IamRole(this, 'codepipeline-role', { + name: `${this.config.prefix}-CodePipelineRole`, + assumeRolePolicy: new DataAwsIamPolicyDocument( + this, + `codepipeline-assume-role-policy`, + { + statement: [ + { + effect: 'Allow', + actions: ['sts:AssumeRole'], + principals: [ + { + identifiers: ['codepipeline.amazonaws.com'], + type: 'Service', + }, + ], + }, + ], + provider: this.config.provider, + }, + ).json, + }); + + new IamRolePolicy(this, 'codepipeline-role-policy', { + name: `${this.config.prefix}-CodePipeline-Role-Policy`, + role: role.id, + policy: new DataAwsIamPolicyDocument( + this, + `codepipeline-role-policy-document`, + { + statement: [ + { + effect: 'Allow', + actions: ['codestar-connections:UseConnection'], + resources: [this.config.source.codeStarConnectionArn], + }, + { + effect: 'Allow', + actions: [ + 'codebuild:BatchGetBuilds', + 'codebuild:StartBuild', + 'codebuild:BatchGetBuildBatches', + 'codebuild:StartBuildBatch', + ], + resources: [ + // The * allows CodeBuild in `preDeployStages` and + // `postDeployStages` to start, if the project name starts with + // `this.config.prefix`. + `arn:aws:codebuild:*:*:project/${this.codeBuildProjectName}*`, + ], + }, + { + effect: 'Allow', + actions: [ + 'codedeploy:CreateDeployment', + 'codedeploy:GetApplication', + 'codedeploy:GetApplicationRevision', + 'codedeploy:GetDeployment', + 'codedeploy:RegisterApplicationRevision', + 'codedeploy:GetDeploymentConfig', + ], + resources: [ + `arn:aws:codedeploy:*:*:application:${this.codeDeployApplicationName}`, + `arn:aws:codedeploy:*:*:deploymentgroup:${this.codeDeployApplicationName}/${this.codeDeployDeploymentGroupName}`, + 'arn:aws:codedeploy:*:*:deploymentconfig:*', + ], + }, + { + effect: 'Allow', + actions: [ + 's3:GetObject', + 's3:GetObjectVersion', + 's3:GetBucketVersioning', + 's3:PutObjectAcl', + 's3:PutObject', + ], + resources: [ + this.pipelineArtifactBucket.arn, + `${this.pipelineArtifactBucket.arn}/*`, + ], + }, + { + effect: 'Allow', + actions: ['iam:PassRole'], + resources: ['*'], + condition: [ + { + variable: 'iam:PassedToService', + test: 'StringEqualsIfExists', + values: ['ecs-tasks.amazonaws.com'], + }, + ], + }, + { + effect: 'Allow', + actions: ['ecs:RegisterTaskDefinition'], + resources: ['*'], + }, + ], + provider: this.config.provider, + }, + ).json, + }); + + return role; + } + + private getArtifactStore = () => [ + { + location: this.pipelineArtifactBucket.bucket, + type: 'S3', + encryptionKey: { id: this.s3KmsAlias.arn, type: 'KMS' }, + }, + ]; + + /** + * Get the source code from GitHub. + * @private + */ + private getSourceStage = () => ({ + name: 'Source', + action: [ + { + name: 'GitHub_Checkout', + category: 'Source', + owner: 'AWS', + provider: 'CodeStarSourceConnection', + version: '1', + outputArtifacts: ['SourceOutput'], + configuration: { + ConnectionArn: this.config.source.codeStarConnectionArn, + FullRepositoryId: this.config.source.repository, + BranchName: this.config.source.branchName, + DetectChanges: 'false', + }, + namespace: 'SourceVariables', + }, + ], + }); + + /** + * Get a stage that deploys the infrastructure and ECS service. + * @private + */ + private getDeployStage = (): CodepipelineStage => ({ + name: 'Deploy', + action: [this.getDeployCdkAction(), this.getDeployEcsAction()], + }); + + /** + * Get the CDK for Terraform deployment step that runs `terraform apply`. + * @private + */ + private getDeployCdkAction = (): CodepipelineStageAction => ({ + name: 'Deploy_CDK', + category: 'Build', + owner: 'AWS', + provider: 'CodeBuild', + inputArtifacts: ['SourceOutput'], + outputArtifacts: ['CodeBuildOutput'], + version: '1', + configuration: { + ProjectName: this.codeBuildProjectName, + EnvironmentVariables: `[${JSON.stringify({ + name: 'GIT_BRANCH', + value: '#{SourceVariables.BranchName}', + })}]`, + }, + runOrder: 1, + }); + + /** + * Get the ECS CodeDeploy step that does a blue/green deployment. + * @private + */ + private getDeployEcsAction = (): CodepipelineStageAction => ({ + name: 'Deploy_ECS', + category: 'Deploy', + owner: 'AWS', + provider: 'CodeDeployToECS', + inputArtifacts: ['CodeBuildOutput'], + version: '1', + configuration: { + ApplicationName: this.codeDeployApplicationName, + DeploymentGroupName: this.codeDeployDeploymentGroupName, + TaskDefinitionTemplateArtifact: 'CodeBuildOutput', + TaskDefinitionTemplatePath: this.taskDefinitionTemplatePath, + AppSpecTemplateArtifact: 'CodeBuildOutput', + AppSpecTemplatePath: this.appSpecTemplatePath, + }, + runOrder: 2, + }); +} diff --git a/packages/terraform-modules/src/pocket/PocketEventBridgeRuleWithMultipleTargets.spec.ts b/packages/terraform-modules/src/pocket/PocketEventBridgeRuleWithMultipleTargets.spec.ts new file mode 100644 index 00000000..e4a6888e --- /dev/null +++ b/packages/terraform-modules/src/pocket/PocketEventBridgeRuleWithMultipleTargets.spec.ts @@ -0,0 +1,144 @@ +import { Testing } from 'cdktf'; +import { + PocketEventBridgeRuleWithMultipleTargets, + PocketEventBridgeProps, +} from './PocketEventBridgeRuleWithMultipleTargets'; +import { + PocketVersionedLambda, + PocketVersionedLambdaProps, +} from './PocketVersionedLambda'; +import { LAMBDA_RUNTIMES } from '../base/ApplicationVersionedLambda'; +import { SqsQueue } from '@cdktf/provider-aws/lib/sqs-queue'; + +describe('PocketEventBridgeRuleWithMultipleTargets', () => { + test('renders an event bridge and multiple targets', () => { + const synthed = Testing.synthScope((stack) => { + const lambdaConfig: PocketVersionedLambdaProps = { + name: 'test-lambda', + lambda: { + runtime: LAMBDA_RUNTIMES.PYTHON38, + handler: 'index.handler', + }, + }; + const testLambda = new PocketVersionedLambda( + stack, + 'test-lambda', + lambdaConfig, + ); + + const testSqs = new SqsQueue(stack, 'test-queue', { + name: 'Test-SQS-Queue', + }); + + const testConfig: PocketEventBridgeProps = { + eventRule: { + name: 'test-event-bridge-rule-multiple-targets', + eventPattern: { + source: ['aws.states'], + 'detail-type': ['Step Functions Execution Status Change'], + }, + }, + targets: [ + { + targetId: 'test-lambda-id', + arn: 'lambda.arn', + terraformResource: testLambda.lambda.versionedLambda, + }, + { + targetId: 'test-sqs-id', + arn: testSqs.arn, + terraformResource: testSqs, + }, + ], + }; + + new PocketEventBridgeRuleWithMultipleTargets( + stack, + 'test-event-bridge-for-multiple-targets-1', + { + ...testConfig, + eventRule: { + ...testConfig.eventRule, + description: 'Test description', + }, + }, + ); + }); + expect(synthed).toMatchSnapshot(); + }); + + test('renders an event bridge and pre-existing targets', () => { + const synthed = Testing.synthScope((stack) => { + const testConfig: PocketEventBridgeProps = { + eventRule: { + name: 'test-event-bridge-rule-multiple-targets', + eventPattern: { + source: ['aws.states'], + 'detail-type': ['Step Functions Execution Status Change'], + }, + }, + targets: [ + { + targetId: 'test-lambda-id', + arn: 'lambda.arn', + }, + { + targetId: 'test-sqs-id', + arn: 'testSqs.arn', + }, + ], + }; + + new PocketEventBridgeRuleWithMultipleTargets( + stack, + 'test-event-bridge-for-multiple-targets-1', + { + ...testConfig, + eventRule: { + ...testConfig.eventRule, + description: 'Test description', + }, + }, + ); + }); + expect(synthed).toMatchSnapshot(); + }); + + test('renders an event bridge rule with prevent destroy flag', () => { + const synthed = Testing.synthScope((stack) => { + const testConfig: PocketEventBridgeProps = { + eventRule: { + name: 'test-event-bridge-rule-multiple-targets', + eventPattern: { + source: ['aws.states'], + 'detail-type': ['Step Functions Execution Status Change'], + }, + preventDestroy: true, + }, + targets: [ + { + targetId: 'test-lambda-id', + arn: 'lambda.arn', + }, + { + targetId: 'test-sqs-id', + arn: 'testSqs.arn', + }, + ], + }; + + new PocketEventBridgeRuleWithMultipleTargets( + stack, + 'test-event-bridge-for-multiple-targets-1', + { + ...testConfig, + eventRule: { + ...testConfig.eventRule, + description: 'Test description', + }, + }, + ); + }); + expect(synthed).toMatchSnapshot(); + }); +}); diff --git a/packages/terraform-modules/src/pocket/PocketEventBridgeRuleWithMultipleTargets.ts b/packages/terraform-modules/src/pocket/PocketEventBridgeRuleWithMultipleTargets.ts new file mode 100644 index 00000000..aef46187 --- /dev/null +++ b/packages/terraform-modules/src/pocket/PocketEventBridgeRuleWithMultipleTargets.ts @@ -0,0 +1,76 @@ +import { TerraformMetaArguments, TerraformResource } from 'cdktf'; +import { Construct } from 'constructs'; +import { + ApplicationEventBridgeRule, + ApplicationEventBridgeRuleProps, + Target, +} from '../base/ApplicationEventBridgeRule'; + +export type PocketEventBridgeTargets = { + targetId: string; + arn: string; + // when the target is a pre-existing construct, we don't need to pass + // in a terraform resource. + terraformResource?: TerraformResource; + deadLetterArn?: string; +}; + +export interface PocketEventBridgeProps extends TerraformMetaArguments { + eventRule: Omit; + targets?: PocketEventBridgeTargets[]; + tags?: { [key: string]: string }; +} + +/** + * class to roll out event bridge rule with multiple AWS resources as targets + * Note: the targets need to be created prior and passed to this function. + * This class does not handle IAM, they have to be handled at the consuming function + */ +export class PocketEventBridgeRuleWithMultipleTargets extends Construct { + private eventBridgeRule: ApplicationEventBridgeRule; + constructor( + scope: Construct, + name: string, + protected readonly config: PocketEventBridgeProps, + ) { + super(scope, name); + this.eventBridgeRule = this.createEventBridgeRule(config.targets); + } + + public getEventBridge() { + return this.eventBridgeRule; + } + + /** + * Creates the actual rule for event bridge to trigger the given target. + * @param eventRuleTargets + * @private + */ + private createEventBridgeRule( + eventRuleTargets?: any, + ): ApplicationEventBridgeRule { + const eventRuleConfig = this.config.eventRule; + const targets: Target[] = []; + + eventRuleTargets.forEach((t) => { + const target: Target = { + targetId: t.targetId, + arn: t.arn, + deadLetterArn: t.deadLetterArn, + }; + + if (t.terraformResource) { + target.dependsOn = t.terraformResource; + } + + targets.push(target); + }); + + return new ApplicationEventBridgeRule(this, 'event-bridge-rule', { + ...eventRuleConfig, + targets: targets, + tags: this.config.tags, + provider: this.config.provider, + }); + } +} diff --git a/packages/terraform-modules/src/pocket/PocketEventBridgeWithLambdaTarget.spec.ts b/packages/terraform-modules/src/pocket/PocketEventBridgeWithLambdaTarget.spec.ts new file mode 100644 index 00000000..395fd1fd --- /dev/null +++ b/packages/terraform-modules/src/pocket/PocketEventBridgeWithLambdaTarget.spec.ts @@ -0,0 +1,40 @@ +import { Testing } from 'cdktf'; +import { + PocketEventBridgeWithLambdaTarget, + PocketEventBridgeWithLambdaTargetProps, +} from './PocketEventBridgeWithLambdaTarget'; +import { LAMBDA_RUNTIMES } from '../base/ApplicationVersionedLambda'; + +const config: PocketEventBridgeWithLambdaTargetProps = { + name: 'test-event-bridge-lambda', + eventRule: { + eventPattern: { + source: ['aws.states'], + 'detail-type': ['Step Functions Execution Status Change'], + }, + }, + lambda: { + runtime: LAMBDA_RUNTIMES.PYTHON38, + handler: 'index.handler', + }, +}; + +test('renders an event bridge and lambda target with rule description', () => { + const synthed = Testing.synthScope((stack) => { + new PocketEventBridgeWithLambdaTarget(stack, 'test-event-bridge-lambda', { + ...config, + eventRule: { ...config.eventRule, description: 'Test description' }, + }); + }); + expect(synthed).toMatchSnapshot(); +}); + +test('renders an event bridge and lambda target with event bus name', () => { + const synthed = Testing.synthScope((stack) => { + new PocketEventBridgeWithLambdaTarget(stack, 'test-event-bridge-lambda', { + ...config, + eventRule: { ...config.eventRule, eventBusName: 'test-bus' }, + }); + }); + expect(synthed).toMatchSnapshot(); +}); diff --git a/packages/terraform-modules/src/pocket/PocketEventBridgeWithLambdaTarget.ts b/packages/terraform-modules/src/pocket/PocketEventBridgeWithLambdaTarget.ts new file mode 100644 index 00000000..3eecb55d --- /dev/null +++ b/packages/terraform-modules/src/pocket/PocketEventBridgeWithLambdaTarget.ts @@ -0,0 +1,86 @@ +import { LambdaPermission } from '@cdktf/provider-aws/lib/lambda-permission'; +import { Construct } from 'constructs'; +import { + ApplicationEventBridgeRule, + ApplicationEventBridgeRuleProps, + Target, +} from '../base/ApplicationEventBridgeRule'; +import { ApplicationVersionedLambda } from '../base/ApplicationVersionedLambda'; +import { + PocketVersionedLambda, + PocketVersionedLambdaProps, +} from './PocketVersionedLambda'; + +export interface PocketEventBridgeWithLambdaTargetProps + extends PocketVersionedLambdaProps { + eventRule: Omit; +} + +/** + * Extends the base pocket versioned lambda class to add an event bridge based trigger on top of the lambda + */ +export class PocketEventBridgeWithLambdaTarget extends PocketVersionedLambda { + constructor( + scope: Construct, + name: string, + protected readonly config: PocketEventBridgeWithLambdaTargetProps, + ) { + super(scope, name, config); + + const eventBridgeRule = this.createEventBridgeRule([this.lambda]); + this.createLambdaEventRuleResourcePermission( + [this.lambda], + eventBridgeRule, + ); + } + + /** + * Creates the approriate permission to allow aws events to invoke lambda + * @param lambda + * @param eventBridgeRule + * @private + */ + private createLambdaEventRuleResourcePermission( + targetLambdas: ApplicationVersionedLambda[], + eventBridgeRule: ApplicationEventBridgeRule, + ): void { + targetLambdas.forEach((lambda) => { + new LambdaPermission(this, 'lambda-permission', { + action: 'lambda:InvokeFunction', + functionName: lambda.versionedLambda.functionName, + qualifier: lambda.versionedLambda.name, + principal: 'events.amazonaws.com', + sourceArn: eventBridgeRule.rule.arn, + dependsOn: [lambda.versionedLambda, eventBridgeRule.rule], + provider: lambda.versionedLambda.provider, + }); + }); + } + + /** + * Creates the actual rule for event bridge to trigger the lambda + * @param lambda + * @private + */ + private createEventBridgeRule( + targetLambdas: ApplicationVersionedLambda[], + ): ApplicationEventBridgeRule { + const eventRuleConfig = this.config.eventRule; + const targets: Target[] = []; + targetLambdas.forEach((t) => + targets.push({ + targetId: 'lambda', + arn: t.versionedLambda.arn, + dependsOn: t.versionedLambda, + }), + ); + + return new ApplicationEventBridgeRule(this, 'event-bridge-rule', { + ...eventRuleConfig, + name: this.config.name, + targets: targets, + tags: this.config.tags, + provider: this.config.provider, + }); + } +} diff --git a/packages/terraform-modules/src/pocket/PocketPagerDuty.spec.ts b/packages/terraform-modules/src/pocket/PocketPagerDuty.spec.ts new file mode 100644 index 00000000..9ec7a068 --- /dev/null +++ b/packages/terraform-modules/src/pocket/PocketPagerDuty.spec.ts @@ -0,0 +1,73 @@ +import { Testing } from 'cdktf'; +import { PocketPagerDuty, PocketPagerDutyProps } from './PocketPagerDuty'; + +const config: PocketPagerDutyProps = { + prefix: 'Test-Env', + service: { + criticalEscalationPolicyId: 'critical-id', + nonCriticalEscalationPolicyId: 'non-critical-id', + }, +}; + +test('renders a Pocket PagerDuty for critical and non-critical actions', () => { + const synthed = Testing.synthScope((stack) => { + new PocketPagerDuty(stack, 'test-pagerduty', config); + }); + expect(synthed).toMatchSnapshot(); +}); + +test('renders a Pocket PagerDuty with custom auto resolve timeout', () => { + const synthed = Testing.synthScope((stack) => { + new PocketPagerDuty(stack, 'test-pagerduty', { + ...config, + service: { + ...config.service, + autoResolveTimeout: 4, + }, + }); + }); + expect(synthed).toMatchSnapshot(); +}); + +test('renders a Pocket PagerDuty with custom acknowledgement timeout', () => { + const synthed = Testing.synthScope((stack) => { + new PocketPagerDuty(stack, 'test-pagerduty', { + ...config, + service: { + ...config.service, + acknowledgementTimeout: 100, + }, + }); + }); + expect(synthed).toMatchSnapshot(); +}); + +test('renders a Pocket PagerDuty with sns topic tags', () => { + const synthed = Testing.synthScope((stack) => { + new PocketPagerDuty(stack, 'test-pagerduty', { + ...config, + sns: { + topic: { + tags: { + Test: 'Topic', + }, + }, + }, + }); + }); + expect(synthed).toMatchSnapshot(); +}); + +test('renders a Pocket PagerDuty with custom sns topic subscription confirmation timeout in minutes', () => { + const synthed = Testing.synthScope((stack) => { + new PocketPagerDuty(stack, 'test-pagerduty', { + ...config, + sns: { + subscription: { + confirmationTimeoutInMinutes: 10, + }, + }, + }); + }); + expect(synthed).toMatchSnapshot(); +}); diff --git a/packages/terraform-modules/src/pocket/PocketPagerDuty.ts b/packages/terraform-modules/src/pocket/PocketPagerDuty.ts new file mode 100644 index 00000000..bf01a973 --- /dev/null +++ b/packages/terraform-modules/src/pocket/PocketPagerDuty.ts @@ -0,0 +1,177 @@ +import { TerraformMetaArguments } from 'cdktf'; +import { Construct } from 'constructs'; +import { SnsTopic } from '@cdktf/provider-aws/lib/sns-topic'; +import { SnsTopicSubscription } from '@cdktf/provider-aws/lib/sns-topic-subscription'; +import { DataPagerdutyVendor } from '@cdktf/provider-pagerduty/lib/data-pagerduty-vendor'; +import { Service } from '@cdktf/provider-pagerduty/lib/service'; +import { ServiceIntegration } from '@cdktf/provider-pagerduty/lib/service-integration'; + +export interface PocketPagerDutyProps extends TerraformMetaArguments { + prefix: string; + service: { + autoResolveTimeout?: number; + acknowledgementTimeout?: number; + criticalEscalationPolicyId: string; + nonCriticalEscalationPolicyId: string; + }; + sns?: { + topic?: { + tags?: { [key: string]: string }; + }; + subscription?: { + confirmationTimeoutInMinutes?: number; + }; + }; +} + +export enum PAGERDUTY_SERVICE_URGENCY { + CRITICAL = 'Critical', + NON_CRITICAL = 'Non-Critical', +} + +export class PocketPagerDuty extends Construct { + static readonly SERVICE_AUTO_RESOLVE_TIMEOUT = '14400'; + static readonly SERVICE_ACKNOWLEDGEMENT_TIMEOUT = '1800'; // 30 minutes + static readonly SNS_SUBSCRIPTION_CONFIRMATION_TIMEOUT_IN_MINUTES = 2; + public readonly snsCriticalAlarmTopic: SnsTopic; + public readonly snsNonCriticalAlarmTopic: SnsTopic; + private config: PocketPagerDutyProps; + + constructor(scope: Construct, name: string, config: PocketPagerDutyProps) { + super(scope, name); + + this.config = config; + + const sentryVendor = this.getVendor('Sentry'); + + const cloudwatchVendor = this.getVendor('Cloudwatch'); + + const pagerDutyCritical = this.createService( + PAGERDUTY_SERVICE_URGENCY.CRITICAL, + ); + + const pagerDutyNonCritical = this.createService( + PAGERDUTY_SERVICE_URGENCY.NON_CRITICAL, + ); + + this.createServiceIntegration( + sentryVendor, + pagerDutyCritical, + PAGERDUTY_SERVICE_URGENCY.CRITICAL, + ); + + this.createServiceIntegration( + sentryVendor, + pagerDutyNonCritical, + PAGERDUTY_SERVICE_URGENCY.NON_CRITICAL, + ); + + const cloudwatchCriticalIntegration = this.createServiceIntegration( + cloudwatchVendor, + pagerDutyCritical, + PAGERDUTY_SERVICE_URGENCY.CRITICAL, + ); + + const cloudwatchNonCriticalIntegration = this.createServiceIntegration( + cloudwatchVendor, + pagerDutyNonCritical, + PAGERDUTY_SERVICE_URGENCY.NON_CRITICAL, + ); + + const snsCriticalAlarmTopic = (this.snsCriticalAlarmTopic = + this.createSnsTopic(PAGERDUTY_SERVICE_URGENCY.CRITICAL)); + + const snsNonCriticalAlarmTopic = (this.snsNonCriticalAlarmTopic = + this.createSnsTopic(PAGERDUTY_SERVICE_URGENCY.NON_CRITICAL)); + + this.createSnsTopicSubscription( + snsCriticalAlarmTopic, + cloudwatchCriticalIntegration, + PAGERDUTY_SERVICE_URGENCY.CRITICAL, + ); + + this.createSnsTopicSubscription( + snsNonCriticalAlarmTopic, + cloudwatchNonCriticalIntegration, + PAGERDUTY_SERVICE_URGENCY.NON_CRITICAL, + ); + } + + private createSnsTopicSubscription( + topic: SnsTopic, + integration: ServiceIntegration, + urgency: PAGERDUTY_SERVICE_URGENCY, + ): SnsTopicSubscription { + return new SnsTopicSubscription( + this, + `alarm-${urgency.toLowerCase()}-subscription`, + { + topicArn: topic.arn, + protocol: 'https', + endpoint: `https://events.pagerduty.com/integration/${integration.integrationKey}/enqueue`, + endpointAutoConfirms: true, + confirmationTimeoutInMinutes: + this.config.sns?.subscription?.confirmationTimeoutInMinutes ?? + PocketPagerDuty.SNS_SUBSCRIPTION_CONFIRMATION_TIMEOUT_IN_MINUTES, + dependsOn: [topic, integration], + provider: this.config.provider, + }, + ); + } + + private createSnsTopic(urgency: PAGERDUTY_SERVICE_URGENCY): SnsTopic { + return new SnsTopic(this, `alarm-${urgency.toLowerCase()}-topic`, { + name: `${this.config.prefix}-Infrastructure-Alarm-${urgency}`, + tags: this.config.sns?.topic?.tags ?? {}, + provider: this.config.provider, + }); + } + + private createServiceIntegration( + vendor: DataPagerdutyVendor, + service: Service, + urgency: PAGERDUTY_SERVICE_URGENCY, + ): ServiceIntegration { + return new ServiceIntegration( + this, + `${vendor.friendlyUniqueId}-${urgency.toLowerCase()}`, + { + name: vendor.name, + service: service.id, + vendor: vendor.id, + dependsOn: [service], + }, + ); + } + + private createService(urgency: PAGERDUTY_SERVICE_URGENCY): Service { + const serviceConfig = this.config.service; + + return new Service(this, `pagerduty-${urgency.toLowerCase()}`, { + name: `${this.config.prefix}-PagerDuty-${urgency}`, + acknowledgementTimeout: + serviceConfig.acknowledgementTimeout?.toString() ?? + PocketPagerDuty.SERVICE_ACKNOWLEDGEMENT_TIMEOUT, + alertCreation: 'create_incidents', + autoResolveTimeout: + serviceConfig.autoResolveTimeout?.toString() ?? + PocketPagerDuty.SERVICE_AUTO_RESOLVE_TIMEOUT, + description: `PagerDuty ${urgency}`, + escalationPolicy: + urgency === PAGERDUTY_SERVICE_URGENCY.CRITICAL + ? serviceConfig.criticalEscalationPolicyId + : serviceConfig.nonCriticalEscalationPolicyId, + incidentUrgencyRule: { + type: 'constant', + urgency: + urgency === PAGERDUTY_SERVICE_URGENCY.CRITICAL ? 'high' : 'low', + }, + }); + } + + private getVendor(name: string): DataPagerdutyVendor { + return new DataPagerdutyVendor(this, name.toLowerCase(), { + name, + }); + } +} diff --git a/packages/terraform-modules/src/pocket/PocketSQSWithLambdaTarget.spec.ts b/packages/terraform-modules/src/pocket/PocketSQSWithLambdaTarget.spec.ts new file mode 100644 index 00000000..5abfe49b --- /dev/null +++ b/packages/terraform-modules/src/pocket/PocketSQSWithLambdaTarget.spec.ts @@ -0,0 +1,80 @@ +import { TerraformStack, Testing } from 'cdktf'; +import { LAMBDA_RUNTIMES } from '../base/ApplicationVersionedLambda'; +import { + PocketSQSWithLambdaTarget, + PocketSQSWithLambdaTargetProps, +} from './PocketSQSWithLambdaTarget'; + +const config: PocketSQSWithLambdaTargetProps = { + name: 'test-sqs-lambda', + lambda: { + runtime: LAMBDA_RUNTIMES.PYTHON38, + handler: 'index.handler', + }, +}; + +test('renders a plain sqs queue and lambda target', () => { + const synthed = Testing.synthScope((stack) => { + new PocketSQSWithLambdaTarget(stack, 'test-sqs-lambda', { + ...config, + }); + }); + expect(synthed).toMatchSnapshot(); +}); + +test('renders a plain sqs queue with a deadletter and lambda target', () => { + const synthed = Testing.synthScope((stack) => { + new PocketSQSWithLambdaTarget(stack, 'test-sqs-lambda', { + ...config, + sqsQueue: { + maxReceiveCount: 3, + }, + }); + }); + expect(synthed).toMatchSnapshot(); +}); + +test('validates batch config errors if no batch window', () => { + const app = Testing.app(); + const stack = new TerraformStack(app, 'test'); + + expect( + () => + new PocketSQSWithLambdaTarget(stack, 'test-sqs-lambda', { + ...config, + sqsQueue: { + maxReceiveCount: 3, + }, + batchSize: 20, + }), + ).toThrow(Error); +}); + +test('validates batch config errors if batch window is less then 1', () => { + const app = Testing.app(); + const stack = new TerraformStack(app, 'test'); + + expect( + () => + new PocketSQSWithLambdaTarget(stack, 'test-sqs-lambda', { + ...config, + sqsQueue: { + maxReceiveCount: 3, + }, + batchSize: 20, + batchWindow: 0, + }), + ).toThrow(Error); +}); + +test('renders a lambda triggered by an existing sqs queue', () => { + const synthed = Testing.synthScope((stack) => { + new PocketSQSWithLambdaTarget(stack, 'test-sqs-lambda', { + ...config, + configFromPreexistingSqsQueue: { + name: 'my-existing-sqs', + }, + }); + }); + expect(synthed).toMatchSnapshot(); +}); diff --git a/packages/terraform-modules/src/pocket/PocketSQSWithLambdaTarget.ts b/packages/terraform-modules/src/pocket/PocketSQSWithLambdaTarget.ts new file mode 100644 index 00000000..cfc3b2cc --- /dev/null +++ b/packages/terraform-modules/src/pocket/PocketSQSWithLambdaTarget.ts @@ -0,0 +1,195 @@ +import { Construct } from 'constructs'; +import { + PocketVersionedLambda, + PocketVersionedLambdaProps, +} from './PocketVersionedLambda'; +import { + ApplicationSQSQueue, + ApplicationSQSQueueProps, +} from '../base/ApplicationSQSQueue'; +import { ApplicationVersionedLambda } from '../base/ApplicationVersionedLambda'; +import { DataAwsIamPolicyDocument } from '@cdktf/provider-aws/lib/data-aws-iam-policy-document'; +import { + DataAwsSqsQueueConfig, + DataAwsSqsQueue, +} from '@cdktf/provider-aws/lib/data-aws-sqs-queue'; +import { IamPolicy } from '@cdktf/provider-aws/lib/iam-policy'; +import { IamRole } from '@cdktf/provider-aws/lib/iam-role'; +import { IamRolePolicyAttachment } from '@cdktf/provider-aws/lib/iam-role-policy-attachment'; +import { LambdaEventSourceMapping } from '@cdktf/provider-aws/lib/lambda-event-source-mapping'; +import { SqsQueue } from '@cdktf/provider-aws/lib/sqs-queue'; + +export interface PocketSQSWithLambdaTargetProps + extends PocketVersionedLambdaProps { + /** + * Set configFromPreexistingSqsQueue to use an existing SQS queue. If not provided, then a queue will be created. + */ + configFromPreexistingSqsQueue?: DataAwsSqsQueueConfig; + /** + * Configure a new SQS queue. Cannot be used in combination with configFromPreexistingSqsQueue. + */ + sqsQueue?: PocketSQSProps; + batchSize?: number; + batchWindow?: number; + // https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/lambda_event_source_mapping#function_response_types + functionResponseTypes?: string[]; +} + +export interface PocketSQSProps { + messageRetentionSeconds?: number; + maxReceiveCount?: number; + maxMessageSize?: number; + delaySeconds?: number; + visibilityTimeoutSeconds?: number; +} + +/** + * Extends the base pocket versioned lambda class to add a sqs based trigger on top of the lambda + */ +export class PocketSQSWithLambdaTarget extends PocketVersionedLambda { + public readonly sqsQueueResource: SqsQueue | DataAwsSqsQueue; + public readonly applicationSqsQueue: ApplicationSQSQueue; + + constructor( + scope: Construct, + name: string, + protected readonly config: PocketSQSWithLambdaTargetProps, + ) { + super(scope, name, config); + PocketSQSWithLambdaTarget.validateEventSourceMappingConfig(config); + + if (config.configFromPreexistingSqsQueue) { + this.sqsQueueResource = this.getExistingSqsQueue({ + ...config.configFromPreexistingSqsQueue, + }); + } else { + this.applicationSqsQueue = this.createSqsQueue({ + ...config.sqsQueue, + name: `${config.name}-Queue`, + tags: config.tags, + provider: this.config.provider, + }); + + this.sqsQueueResource = this.applicationSqsQueue.sqsQueue; + } + + this.createSQSExecutionPolicyOnLambda( + this.lambda.lambdaExecutionRole, + this.sqsQueueResource, + ); + + this.createEventSourceMapping(this.lambda, this.sqsQueueResource, config); + } + + /** + * Creates the sqs queue to use with the lambda target + * @private + */ + private createSqsQueue( + sqsQueueConfig: ApplicationSQSQueueProps, + ): ApplicationSQSQueue { + return new ApplicationSQSQueue(this, 'lambda_sqs_queue', sqsQueueConfig); + } + + /** + * Creates the sqs queue to use with the lambda target + * @private + */ + private getExistingSqsQueue( + sqsQueueConfig: DataAwsSqsQueueConfig, + ): DataAwsSqsQueue { + return new DataAwsSqsQueue(this, 'lambda_sqs_queue', sqsQueueConfig); + } + + /** + * Validates the event source mapping config. + * These values are defined by aws here: https://docs.aws.amazon.com/lambda/latest/dg/with-html + * @param config + * @private + */ + private static validateEventSourceMappingConfig( + config: PocketSQSWithLambdaTargetProps, + ) { + if ( + config.batchSize && + config.batchSize > 10 && + (!config.batchWindow || config.batchWindow < 1) + ) { + throw new Error( + 'Maximum batch window in seconds must be greater than 0 if maximum batch size is greater than 10', + ); + } + + if (config.configFromPreexistingSqsQueue && config.sqsQueue) { + throw new Error( + 'configFromPreexistingSqsQueue and sqsQueue cannot be used simultaneously.', + ); + } + } + + /** + * Creates the event source mapping for SQS to lambda + * @param lambda + * @param sqsQueue + * @param config + * @private + */ + private createEventSourceMapping( + lambda: ApplicationVersionedLambda, + sqsQueue: SqsQueue | DataAwsSqsQueue, + config: PocketSQSWithLambdaTargetProps, + ) { + return new LambdaEventSourceMapping(this, `lambda_event_source_mapping`, { + eventSourceArn: sqsQueue.arn, + functionName: lambda.versionedLambda.arn, + batchSize: config.batchSize, + maximumBatchingWindowInSeconds: config.batchWindow, + functionResponseTypes: config.functionResponseTypes, + provider: config.provider, + }); + } + + /** + * Allow the Lambda to access the sqs queue + * @param executionRole + * @param sqsQueue + * @private + */ + private createSQSExecutionPolicyOnLambda( + executionRole: IamRole, + sqsQueue: SqsQueue | DataAwsSqsQueue, + ): IamRolePolicyAttachment { + const lambdaSqsPolicy = new IamPolicy(this, 'sqs-policy', { + name: `${this.config.name}-LambdaSQSPolicy`, + policy: new DataAwsIamPolicyDocument(this, `lambda_sqs_policy`, { + statement: [ + { + effect: 'Allow', + actions: [ + 'sqs:SendMessage', + 'sqs:ReceiveMessage', + 'sqs:DeleteMessage', + 'sqs:GetQueueAttributes', + 'sqs:ChangeMessageVisibility', + ], + resources: [sqsQueue.arn], + }, + ], + }).json, + dependsOn: [executionRole], + provider: this.config.provider, + tags: this.config.tags, + }); + + return new IamRolePolicyAttachment( + this, + 'execution-role-policy-attachment', + { + role: executionRole.name, + policyArn: lambdaSqsPolicy.arn, + dependsOn: [executionRole, lambdaSqsPolicy], + provider: this.config.provider, + }, + ); + } +} diff --git a/packages/terraform-modules/src/pocket/PocketSynthetics.spec.ts b/packages/terraform-modules/src/pocket/PocketSynthetics.spec.ts new file mode 100644 index 00000000..507126cc --- /dev/null +++ b/packages/terraform-modules/src/pocket/PocketSynthetics.spec.ts @@ -0,0 +1,22 @@ +import { Testing } from 'cdktf'; +import { PocketSyntheticProps, PocketSyntheticCheck } from './PocketSynthetics'; + +const config: PocketSyntheticProps = { + uri: 'acme.getpocket.dev', + verifySsl: true, +}; + +it('renders a Pocket New Relic synthetic check', () => { + const synthed = Testing.synthScope((stack) => { + new PocketSyntheticCheck(stack, 'test-synthetic', config); + }); + expect(synthed).toMatchSnapshot(); +}); + +it('allows passing different values for nrql config', () => { + config.nrqlConfig.query = 'SELECT * FROM MY-COOL-TABLE'; + const synthed = Testing.synthScope((stack) => { + new PocketSyntheticCheck(stack, 'test-synthetic', config); + }); + expect(synthed).toMatchSnapshot(); +}); diff --git a/packages/terraform-modules/src/pocket/PocketSynthetics.ts b/packages/terraform-modules/src/pocket/PocketSynthetics.ts new file mode 100644 index 00000000..9283c4b9 --- /dev/null +++ b/packages/terraform-modules/src/pocket/PocketSynthetics.ts @@ -0,0 +1,112 @@ +import { NrqlAlertCondition } from '@cdktf/provider-newrelic/lib/nrql-alert-condition'; +import { SyntheticsMonitor } from '@cdktf/provider-newrelic/lib/synthetics-monitor'; +import { Construct } from 'constructs'; + +export interface PocketSyntheticProps { + uri: string; + verifySsl: boolean; + policyId?: number; + nrqlConfig?: { + query?: string; + evaluationOffset?: number; + violationTimeLimitSeconds?: number; + closeViolationsOnExpiration?: boolean; + expirationDuration?: number; + slideBy: number; + aggregationWindow: number; + aggregationMethod: string; + aggregationDelay: string; + critical?: { + operator?: string; + threshold?: number; + thresholdDuration?: number; + thresholdOccurrences?: string; + }; + }; +} + +const globalCheckLocations = [ + 'AWS_US_WEST_2', + 'AWS_EU_WEST_2', + 'AWS_US_EAST_2', +]; + +export class PocketSyntheticCheck extends Construct { + constructor( + scope: Construct, + private name: string, + private config: PocketSyntheticProps, + ) { + super(scope, name); + + // if policy id not provided use default policy id + // policy in another + if (this.config.policyId === undefined) { + // I wanted to do a terraform lookup but it doesn't run before generating + // the cdktf.json file so we have to hardcode the default policy id + this.config.policyId = 1707149; // Pocket-Default-Policy + } + + const pocketMonitor = new SyntheticsMonitor( + this, + `${this.name}-synthetics-monitor`, + { + name: `${this.name}-synthetics`, + type: 'SIMPLE', + period: 'EVERY_5_MINUTES', + status: 'ENABLED', + locationsPublic: globalCheckLocations, + uri: this.config.uri, + verifySsl: this.config.verifySsl, + }, + ); + + const defaultNrqlConfig = { + query: `SELECT count(result) from SyntheticCheck where result = 'FAILED' and monitorName = '${pocketMonitor.name}'`, + aggregationMethod: 'cadence', + aggregationWindow: 3000, + aggregationDelay: '180', + slideBy: 60, + violationTimeLimitSeconds: 2592000, + closeViolationsOnExpiration: true, + expirationDuration: 600, + critical: { + operator: 'above', + threshold: 2, + thresholdDuration: 900, + thresholdOccurrences: 'AT_LEAST_ONCE', + }, + }; + + this.config.nrqlConfig = { + ...defaultNrqlConfig, + ...config.nrqlConfig, + }; + + new NrqlAlertCondition(this, 'alert-condition', { + name: `${this.name}-nrql`, + policyId: this.config.policyId, + fillValue: 0, + fillOption: 'static', + aggregationWindow: this.config.nrqlConfig.aggregationWindow, + aggregationMethod: this.config.nrqlConfig.aggregationMethod, + aggregationDelay: this.config.nrqlConfig.aggregationDelay, + slideBy: this.config.nrqlConfig.slideBy, + nrql: { + query: this.config.nrqlConfig.query, + }, + violationTimeLimitSeconds: + this.config.nrqlConfig.violationTimeLimitSeconds, + closeViolationsOnExpiration: + this.config.nrqlConfig.closeViolationsOnExpiration, + expirationDuration: this.config.nrqlConfig.expirationDuration, + critical: { + operator: this.config.nrqlConfig.critical.operator, + threshold: this.config.nrqlConfig.critical.threshold, + thresholdDuration: this.config.nrqlConfig.critical.thresholdDuration, + thresholdOccurrences: + this.config.nrqlConfig.critical.thresholdOccurrences, + }, + }); + } +} diff --git a/packages/terraform-modules/src/pocket/PocketVPC.spec.ts b/packages/terraform-modules/src/pocket/PocketVPC.spec.ts new file mode 100644 index 00000000..eb22c789 --- /dev/null +++ b/packages/terraform-modules/src/pocket/PocketVPC.spec.ts @@ -0,0 +1,13 @@ +import { Testing } from 'cdktf'; +import { PocketVPC } from './PocketVPC'; +import 'cdktf/lib/testing/adapters/jest'; +import { DataAwsVpc } from '@cdktf/provider-aws/lib/data-aws-vpc'; + +test('renders a VPC with minimal config', () => { + const synthed = Testing.synthScope((stack) => { + new PocketVPC(stack, 'testPocketVPC'); + }); + + expect(synthed).toMatchSnapshot(); + expect(synthed).toHaveDataSource(DataAwsVpc); +}); diff --git a/packages/terraform-modules/src/pocket/PocketVPC.ts b/packages/terraform-modules/src/pocket/PocketVPC.ts new file mode 100644 index 00000000..b809f4ae --- /dev/null +++ b/packages/terraform-modules/src/pocket/PocketVPC.ts @@ -0,0 +1,132 @@ +import { DataAwsCallerIdentity } from '@cdktf/provider-aws/lib/data-aws-caller-identity'; +import { DataAwsKmsAlias } from '@cdktf/provider-aws/lib/data-aws-kms-alias'; +import { DataAwsRegion } from '@cdktf/provider-aws/lib/data-aws-region'; +import { DataAwsSecurityGroups } from '@cdktf/provider-aws/lib/data-aws-security-groups'; +import { DataAwsSsmParameter } from '@cdktf/provider-aws/lib/data-aws-ssm-parameter'; +import { DataAwsSubnets } from '@cdktf/provider-aws/lib/data-aws-subnets'; +import { DataAwsVpc } from '@cdktf/provider-aws/lib/data-aws-vpc'; +import { AwsProvider } from '@cdktf/provider-aws/lib/provider'; +import { Fn } from 'cdktf'; +import { Construct } from 'constructs'; + +export class PocketVPC extends Construct { + public readonly vpc: DataAwsVpc; + + public readonly region: string; + public readonly accountId: string; + public readonly privateSubnetIds: string[]; + public readonly publicSubnetIds: string[]; + public readonly secretsManagerSecretKey: DataAwsKmsAlias; + public readonly defaultSecurityGroups: DataAwsSecurityGroups; + public readonly internalSecurityGroups: DataAwsSecurityGroups; + + constructor(scope: Construct, name: string, provider?: AwsProvider) { + super(scope, name); + + const vpcSSMParam = new DataAwsSsmParameter(this, `vpc_ssm_param`, { + provider: provider, + name: '/Shared/Vpc', + }); + + this.vpc = new DataAwsVpc(this, `vpc`, { + provider: provider, + filter: [ + { + name: 'vpc-id', + values: [vpcSSMParam.value], + }, + ], + }); + + const privateString = new DataAwsSsmParameter(this, `private_subnets`, { + provider: provider, + name: '/Shared/PrivateSubnets', + }); + + const privateSubnets = new DataAwsSubnets(this, `private_subnet_ids`, { + provider: provider, + filter: [ + { + name: 'subnet-id', + values: Fn.split(',', privateString.value), + }, + { name: 'vpc-id', values: [this.vpc.id] }, + ], + }); + + this.privateSubnetIds = privateSubnets.ids; + + const publicString = new DataAwsSsmParameter(this, `public_subnets`, { + provider: provider, + name: '/Shared/PublicSubnets', + }); + + const publicSubnets = new DataAwsSubnets(this, `public_subnet_ids`, { + provider: provider, + filter: [ + { + name: 'subnet-id', + values: Fn.split(',', publicString.value), + }, + { name: 'vpc-id', values: [this.vpc.id] }, + ], + }); + + this.publicSubnetIds = publicSubnets.ids; + + const identity = new DataAwsCallerIdentity(this, `current_identity`, { + provider: provider, + }); + this.accountId = identity.accountId; + + const region = new DataAwsRegion(this, 'current_region', { + provider: provider, + }); + this.region = region.name; + + this.secretsManagerSecretKey = new DataAwsKmsAlias( + this, + 'secrets_manager_key', + { + provider: provider, + name: 'alias/aws/secretsmanager', + }, + ); + + this.defaultSecurityGroups = new DataAwsSecurityGroups( + this, + 'default_security_groups', + { + provider: provider, + filter: [ + { + name: 'group-name', + values: ['default'], + }, + { + name: 'vpc-id', + values: [this.vpc.id], + }, + ], + }, + ); + + this.internalSecurityGroups = new DataAwsSecurityGroups( + this, + 'internal_security_groups', + { + provider: provider, + filter: [ + { + name: 'group-name', + values: ['pocket-vpc-internal'], + }, + { + name: 'vpc-id', + values: [this.vpc.id], + }, + ], + }, + ); + } +} diff --git a/packages/terraform-modules/src/pocket/PocketVersionedLambda.spec.ts b/packages/terraform-modules/src/pocket/PocketVersionedLambda.spec.ts new file mode 100644 index 00000000..2e8b522b --- /dev/null +++ b/packages/terraform-modules/src/pocket/PocketVersionedLambda.spec.ts @@ -0,0 +1,283 @@ +import { Testing, TerraformStack } from 'cdktf'; +import { + PocketVersionedLambda, + PocketVersionedLambdaProps, +} from './PocketVersionedLambda'; +import { LAMBDA_RUNTIMES } from '../base/ApplicationVersionedLambda'; + +const config: PocketVersionedLambdaProps = { + name: 'test-lambda', + lambda: { + runtime: LAMBDA_RUNTIMES.PYTHON38, + handler: 'index.handler', + }, +}; + +test('renders a lambda target', () => { + const synthed = Testing.synthScope((stack) => { + new PocketVersionedLambda(stack, 'test-lambda', config); + }); + expect(synthed).toMatchSnapshot(); +}); + +test('renders a lambda target with tags', () => { + const synthed = Testing.synthScope((stack) => { + new PocketVersionedLambda(stack, 'test-lambda', { + ...config, + tags: { + my: 'tags', + for: 'test', + }, + }); + }); + expect(synthed).toMatchSnapshot(); +}); + +test('renders a lambda with lambda description', () => { + const synthed = Testing.synthScope((stack) => { + new PocketVersionedLambda(stack, 'test-lambda', { + ...config, + lambda: { ...config.lambda, description: 'Test description' }, + }); + }); + expect(synthed).toMatchSnapshot(); +}); + +test('renders a lambda with timeout', () => { + const synthed = Testing.synthScope((stack) => { + new PocketVersionedLambda(stack, 'test-lambda', { + ...config, + lambda: { ...config.lambda, timeout: 300 }, + }); + }); + expect(synthed).toMatchSnapshot(); +}); + +test('renders a lambda with environment variables', () => { + const synthed = Testing.synthScope((stack) => { + new PocketVersionedLambda(stack, 'test-lambda', { + ...config, + lambda: { + ...config.lambda, + environment: { + my: 'var', + IS: 'good', + }, + }, + }); + }); + expect(synthed).toMatchSnapshot(); +}); + +test('renders a lambda with vpc config', () => { + const synthed = Testing.synthScope((stack) => { + new PocketVersionedLambda(stack, 'test-lambda', { + ...config, + lambda: { + ...config.lambda, + vpcConfig: { + subnetIds: ['1', '2'], + securityGroupIds: ['sec1', 'sec2'], + }, + }, + }); + }); + expect(synthed).toMatchSnapshot(); +}); + +test('renders a lambda with s3 bucket', () => { + const synthed = Testing.synthScope((stack) => { + new PocketVersionedLambda(stack, 'test-lambda', { + ...config, + lambda: { + ...config.lambda, + s3Bucket: 'test-bucket', + }, + }); + }); + expect(synthed).toMatchSnapshot(); +}); + +test('renders a lambda with log retention', () => { + const synthed = Testing.synthScope((stack) => { + new PocketVersionedLambda(stack, 'test-lambda', { + ...config, + lambda: { + ...config.lambda, + logRetention: 10, + }, + }); + }); + expect(synthed).toMatchSnapshot(); +}); + +test('renders a lambda with execution policy', () => { + const synthed = Testing.synthScope((stack) => { + new PocketVersionedLambda(stack, 'test-lambda', { + ...config, + lambda: { + ...config.lambda, + executionPolicyStatements: [ + { + effect: 'Allow', + actions: ['*'], + resources: ['*'], + }, + ], + }, + }); + }); + expect(synthed).toMatchSnapshot(); +}); + +test('renders a lambda with code deploy', () => { + const synthed = Testing.synthScope((stack) => { + new PocketVersionedLambda(stack, 'test-lambda', { + ...config, + lambda: { + ...config.lambda, + codeDeploy: { + deploySnsTopicArn: 'arn:test', + detailType: 'FULL', + region: 'us-east-1', + accountId: 'test-account', + }, + }, + }); + }); + expect(synthed).toMatchSnapshot(); +}); + +test('renders a lambda with code deploy with all deploy notifications turned on', () => { + const synthed = Testing.synthScope((stack) => { + new PocketVersionedLambda(stack, 'test-lambda', { + ...config, + lambda: { + ...config.lambda, + codeDeploy: { + deploySnsTopicArn: 'arn:test', + detailType: 'FULL', + region: 'us-east-1', + accountId: 'test-account', + notifications: { + // omitting notifyOnFailed since it's set to true by default if not provided + notifyOnStarted: true, + notifyOnSucceeded: true, + }, + }, + }, + }); + }); + expect(synthed).toMatchSnapshot(); +}); + +test('renders a lambda with alarms', () => { + const synthed = Testing.synthScope((stack) => { + new PocketVersionedLambda(stack, 'test-lambda', { + ...config, + lambda: { + ...config.lambda, + alarms: { + invocations: { + period: 60, + threshold: 1, + }, + errors: { + period: 60, + threshold: 1, + }, + throttles: { + period: 60, + threshold: 1, + }, + concurrentExecutions: { + period: 60, + threshold: 1, + }, + duration: { + period: 60, + threshold: 1, + }, + }, + }, + }); + }); + expect(synthed).toMatchSnapshot(); +}); + +test('it can treat missing data as breaching', () => { + const synthed = Testing.synthScope((stack) => { + new PocketVersionedLambda(stack, 'test-lambda', { + ...config, + lambda: { + ...config.lambda, + alarms: { + invocations: { + period: 60, + threshold: 1, + treatMissingData: 'breaching', + }, + }, + }, + }); + }); + expect(synthed).toMatchSnapshot(); +}); + +test('renders a lambda with concurrencyLimit', () => { + const synthed = Testing.synthScope((stack) => { + new PocketVersionedLambda(stack, 'test-lambda', { + ...config, + lambda: { + ...config.lambda, + reservedConcurrencyLimit: 10, + }, + }); + }); + expect(synthed).toMatchSnapshot(); +}); + +test('renders a lambda with memorySizeInMb', () => { + const synthed = Testing.synthScope((stack) => { + new PocketVersionedLambda(stack, 'test-lambda', { + ...config, + lambda: { + ...config.lambda, + memorySizeInMb: 512, + }, + }); + }); + expect(synthed).toMatchSnapshot(); +}); + +const testAlarmValidation = (alarmType: string) => { + const app = Testing.app(); + const stack = new TerraformStack(app, 'test'); + + const eventBridgeLambdaConfig = { + ...config, + lambda: { + ...config.lambda, + alarms: { + [alarmType]: { + period: 60, + threshold: 1, + datapointsToAlarm: 2, + }, + }, + }, + }; + + expect( + () => + new PocketVersionedLambda(stack, 'test-lambda', eventBridgeLambdaConfig), + ).toThrow(Error); +}; + +test('it validates alarms config', () => { + testAlarmValidation('invocations'); + testAlarmValidation('throttles'); + testAlarmValidation('concurrentExecutions'); + testAlarmValidation('errors'); + testAlarmValidation('duration'); +}); diff --git a/packages/terraform-modules/src/pocket/PocketVersionedLambda.ts b/packages/terraform-modules/src/pocket/PocketVersionedLambda.ts new file mode 100644 index 00000000..0ddea71d --- /dev/null +++ b/packages/terraform-modules/src/pocket/PocketVersionedLambda.ts @@ -0,0 +1,204 @@ +import { TerraformMetaArguments } from 'cdktf'; +import { Construct } from 'constructs'; +import { + ApplicationVersionedLambda, + LAMBDA_RUNTIMES, +} from '../base/ApplicationVersionedLambda'; +import { ApplicationLambdaCodeDeploy } from '../base/ApplicationLambdaCodeDeploy'; +import { CloudwatchMetricAlarm } from '@cdktf/provider-aws/lib/cloudwatch-metric-alarm'; +import { DataAwsIamPolicyDocumentStatement } from '@cdktf/provider-aws/lib/data-aws-iam-policy-document'; +import { LambdaFunctionVpcConfig } from '@cdktf/provider-aws/lib/lambda-function'; + +export interface PocketVersionedLambdaDefaultAlarmProps { + threshold: number; + period: number; + evaluationPeriods?: number; + datapointsToAlarm?: number; + comparisonOperator?: + | 'GreaterThanOrEqualToThreshold' + | 'GreaterThanThreshold' + | 'GreaterThanUpperThreshold' + | 'LessThanLowerOrGreaterThanUpperThreshold' + | 'LessThanLowerThreshold' + | 'LessThanOrEqualToThreshold' + | 'LessThanThreshold'; + alarmDescription?: string; + actions?: string[]; + treatMissingData?: 'missing' | 'notBreaching' | 'breaching' | 'ignore'; +} + +export interface PocketVersionedLambdaProps extends TerraformMetaArguments { + name: string; + lambda: { + description?: string; + runtime: LAMBDA_RUNTIMES; + handler: string; + timeout?: number; + reservedConcurrencyLimit?: number; + memorySizeInMb?: number; + environment?: { [key: string]: string }; + vpcConfig?: LambdaFunctionVpcConfig; + executionPolicyStatements?: DataAwsIamPolicyDocumentStatement[]; + logRetention?: number; + s3Bucket?: string; + codeDeploy?: { + deploySnsTopicArn?: string; + detailType?: 'BASIC' | 'FULL'; + region: string; + accountId: string; + notifications?: { + notifyOnStarted?: boolean; + notifyOnSucceeded?: boolean; + notifyOnFailed?: boolean; + }; + }; + alarms?: { + invocations?: PocketVersionedLambdaDefaultAlarmProps; + errors?: PocketVersionedLambdaDefaultAlarmProps; + concurrentExecutions?: PocketVersionedLambdaDefaultAlarmProps; + throttles?: PocketVersionedLambdaDefaultAlarmProps; + duration?: PocketVersionedLambdaDefaultAlarmProps; + }; + }; + tags?: { [key: string]: string }; +} + +export class PocketVersionedLambda extends Construct { + public readonly lambda: ApplicationVersionedLambda; + constructor( + scope: Construct, + name: string, + protected readonly config: PocketVersionedLambdaProps, + ) { + super(scope, name); + + PocketVersionedLambda.validateConfig(config); + + this.lambda = this.createVersionedLambda(); + + if (config.lambda.codeDeploy) { + this.createLambdaCodeDeploy(); + } + + if (config.lambda.alarms) { + this.createLambdaAlarms(this.lambda); + } + } + + private static validateConfig(config: PocketVersionedLambdaProps): void { + if (!config.lambda.alarms) return; + + const alarms = { + invocations: 'Invocations', + errors: 'Errors', + concurrentExecutions: 'Concurrent Executions', + throttles: 'Throttles', + duration: 'Duration', + }; + + const errorMessage = + 'DatapointsToAlarm must be less than or equal to EvaluationPeriods'; + + const alarmsConfig = config.lambda.alarms; + + Object.keys(alarms).forEach((key) => { + if ( + alarmsConfig[key]?.datapointsToAlarm > + (alarmsConfig[key]?.evaluationPeriods ?? 1) + ) { + throw new Error(`${alarms[key]} Alarm: ${errorMessage}`); + } + }); + } + + private createLambdaAlarms(lambda: ApplicationVersionedLambda): void { + const alarmsConfig = this.config.lambda.alarms; + + const alarms = { + invocations: 'Invocations', + errors: 'Errors', + concurrentExecutions: 'ConcurrentExecutions', + throttles: 'Throttles', + duration: 'Duration', + }; + + Object.keys(alarms).forEach((name) => { + if (alarmsConfig[name]) { + this.createLambdaAlarm(lambda, { + metricName: alarms[name], + props: this.config.lambda.alarms[name], + }); + } + }); + } + + private createLambdaAlarm( + lambda: ApplicationVersionedLambda, + config: { + metricName: string; + props: PocketVersionedLambdaDefaultAlarmProps; + }, + ): void { + const props = config.props; + const defaultEvaluationPeriods = 1; + + new CloudwatchMetricAlarm(this, config.metricName.toLowerCase(), { + alarmName: `${this.config.name}-Lambda-${config.metricName}-Alarm`, + namespace: 'AWS/Lambda', + metricName: config.metricName, + dimensions: { + FunctionName: lambda.versionedLambda.functionName, + Resource: `${lambda.versionedLambda.functionName}:${lambda.versionedLambda.name}`, + }, + period: props.period, + evaluationPeriods: props.evaluationPeriods ?? defaultEvaluationPeriods, + datapointsToAlarm: props.datapointsToAlarm ?? defaultEvaluationPeriods, + statistic: 'Sum', + comparisonOperator: props.comparisonOperator ?? 'GreaterThanThreshold', + threshold: props.threshold, + alarmDescription: + props.alarmDescription ?? + `Total ${config.metricName.toLowerCase()} breaches threshold`, + insufficientDataActions: [], + alarmActions: props.actions ?? [], + okActions: props.actions ?? [], + tags: this.config.tags, + treatMissingData: props.treatMissingData ?? 'missing', + }); + } + + private createLambdaCodeDeploy(): void { + const lambdaConfig = this.config.lambda; + + new ApplicationLambdaCodeDeploy(this, 'lambda-code-deploy', { + name: this.config.name, + deploySnsTopicArn: lambdaConfig.codeDeploy.deploySnsTopicArn, + detailType: lambdaConfig.codeDeploy.detailType, + region: lambdaConfig.codeDeploy.region, + accountId: lambdaConfig.codeDeploy.accountId, + notifications: lambdaConfig.codeDeploy.notifications, + }); + } + + private createVersionedLambda(): ApplicationVersionedLambda { + const lambdaConfig = this.config.lambda; + + return new ApplicationVersionedLambda(this, 'lambda', { + name: this.config.name, + description: lambdaConfig.description, + runtime: lambdaConfig.runtime, + handler: lambdaConfig.handler, + timeout: lambdaConfig.timeout, + environment: lambdaConfig.environment, + vpcConfig: lambdaConfig.vpcConfig, + executionPolicyStatements: lambdaConfig.executionPolicyStatements, + logRetention: lambdaConfig.logRetention, + s3Bucket: + lambdaConfig.s3Bucket ?? `pocket-${this.config.name.toLowerCase()}`, + tags: this.config.tags, + usesCodeDeploy: !!lambdaConfig.codeDeploy, + memorySizeInMb: lambdaConfig.memorySizeInMb, + reservedConcurrencyLimit: lambdaConfig.reservedConcurrencyLimit, + }); + } +} diff --git a/packages/terraform-modules/src/pocket/__snapshots__/PocketALBApplication.spec.ts.snap b/packages/terraform-modules/src/pocket/__snapshots__/PocketALBApplication.spec.ts.snap new file mode 100644 index 00000000..dc1f01ab --- /dev/null +++ b/packages/terraform-modules/src/pocket/__snapshots__/PocketALBApplication.spec.ts.snap @@ -0,0 +1,10737 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`PocketALBApplication renders a Pocket App with attached persistent storage 1`] = ` +"{ + "data": { + "aws_caller_identity": { + "testPocketApp_pocket_vpc_current_identity_06D87057": { + } + }, + "aws_iam_policy_document": { + "testPocketApp_ecs_service_ecs-iam_ecs-task-assume_ABB81680": { + "statement": [ + { + "actions": [ + "sts:AssumeRole" + ], + "effect": "Allow", + "principals": [ + { + "identifiers": [ + "ecs-tasks.amazonaws.com" + ], + "type": "Service" + } + ] + } + ], + "version": "2012-10-17" + } + }, + "aws_kms_alias": { + "testPocketApp_pocket_vpc_secrets_manager_key_9FD2BC93": { + "name": "alias/aws/secretsmanager" + } + }, + "aws_region": { + "testPocketApp_pocket_vpc_current_region_8ED435E7": { + } + }, + "aws_route53_zone": { + "testPocketApp_base_dns_main_hosted_zone_9442EEB0": { + "name": "bowling.gov" + } + }, + "aws_security_groups": { + "testPocketApp_pocket_vpc_default_security_groups_40B4FC48": { + "filter": [ + { + "name": "group-name", + "values": [ + "default" + ] + }, + { + "name": "vpc-id", + "values": [ + "\${data.aws_vpc.testPocketApp_pocket_vpc_C4E157E3.id}" + ] + } + ] + }, + "testPocketApp_pocket_vpc_internal_security_groups_12F7F666": { + "filter": [ + { + "name": "group-name", + "values": [ + "pocket-vpc-internal" + ] + }, + { + "name": "vpc-id", + "values": [ + "\${data.aws_vpc.testPocketApp_pocket_vpc_C4E157E3.id}" + ] + } + ] + } + }, + "aws_ssm_parameter": { + "testPocketApp_pocket_vpc_private_subnets_9D449563": { + "name": "/Shared/PrivateSubnets" + }, + "testPocketApp_pocket_vpc_public_subnets_B0B3A6AB": { + "name": "/Shared/PublicSubnets" + }, + "testPocketApp_pocket_vpc_vpc_ssm_param_235525D4": { + "name": "/Shared/Vpc" + } + }, + "aws_subnets": { + "testPocketApp_pocket_vpc_private_subnet_ids_EB1E3A65": { + "filter": [ + { + "name": "subnet-id", + "values": "\${split(\\",\\", data.aws_ssm_parameter.testPocketApp_pocket_vpc_private_subnets_9D449563.value)}" + }, + { + "name": "vpc-id", + "values": [ + "\${data.aws_vpc.testPocketApp_pocket_vpc_C4E157E3.id}" + ] + } + ] + }, + "testPocketApp_pocket_vpc_public_subnet_ids_01F0B902": { + "filter": [ + { + "name": "subnet-id", + "values": "\${split(\\",\\", data.aws_ssm_parameter.testPocketApp_pocket_vpc_public_subnets_B0B3A6AB.value)}" + }, + { + "name": "vpc-id", + "values": [ + "\${data.aws_vpc.testPocketApp_pocket_vpc_C4E157E3.id}" + ] + } + ] + } + }, + "aws_vpc": { + "testPocketApp_pocket_vpc_C4E157E3": { + "filter": [ + { + "name": "vpc-id", + "values": [ + "\${data.aws_ssm_parameter.testPocketApp_pocket_vpc_vpc_ssm_param_235525D4.value}" + ] + } + ] + } + } + }, + "resource": { + "aws_acm_certificate": { + "testPocketApp_alb_certificate_417C14FF": { + "domain_name": "testing.bowling.gov", + "lifecycle": { + "create_before_destroy": true + }, + "validation_method": "DNS" + } + }, + "aws_acm_certificate_validation": { + "testPocketApp_alb_certificate_certificate_validation_7D899C6A": { + "certificate_arn": "\${aws_acm_certificate.testPocketApp_alb_certificate_417C14FF.arn}", + "depends_on": [ + "aws_route53_record.testPocketApp_alb_certificate_certificate_record_3C49201F", + "aws_acm_certificate.testPocketApp_alb_certificate_417C14FF" + ], + "validation_record_fqdns": [ + "\${aws_route53_record.testPocketApp_alb_certificate_certificate_record_3C49201F.fqdn}" + ] + } + }, + "aws_alb": { + "testPocketApp_application_load_balancer_alb_D538DEEE": { + "internal": false, + "name_prefix": "TSTAPP", + "security_groups": [ + "\${aws_security_group.testPocketApp_application_load_balancer_alb_security_group_91A27046.id}" + ], + "subnets": "\${data.aws_subnets.testPocketApp_pocket_vpc_public_subnet_ids_01F0B902.ids}" + } + }, + "aws_alb_listener": { + "testPocketApp_listener_http_A9E227C2": { + "default_action": [ + { + "redirect": { + "port": "443", + "protocol": "HTTPS", + "status_code": "HTTP_301" + }, + "type": "redirect" + } + ], + "load_balancer_arn": "\${aws_alb.testPocketApp_application_load_balancer_alb_D538DEEE.arn}", + "port": 80, + "protocol": "HTTP" + }, + "testPocketApp_listener_https_7F49DE83": { + "certificate_arn": "\${aws_acm_certificate.testPocketApp_alb_certificate_417C14FF.arn}", + "default_action": [ + { + "fixed_response": { + "content_type": "text/plain", + "message_body": "", + "status_code": "503" + }, + "type": "fixed-response" + } + ], + "load_balancer_arn": "\${aws_alb.testPocketApp_application_load_balancer_alb_D538DEEE.arn}", + "port": 443, + "protocol": "HTTPS", + "ssl_policy": "ELBSecurityPolicy-TLS-1-1-2017-01" + } + }, + "aws_alb_listener_rule": { + "testPocketApp_ecs_service_listener_rule_C03BB04D": { + "action": [ + { + "target_group_arn": "\${aws_alb_target_group.testPocketApp_ecs_service_blue_target_group_ecs_target_group_A4B85885.arn}", + "type": "forward" + } + ], + "condition": [ + { + "path_pattern": { + "values": [ + "*" + ] + } + } + ], + "lifecycle": { + "ignore_changes": [ + "action" + ] + }, + "listener_arn": "\${aws_alb_listener.testPocketApp_listener_https_7F49DE83.arn}", + "priority": 1 + } + }, + "aws_alb_target_group": { + "testPocketApp_ecs_service_blue_target_group_ecs_target_group_A4B85885": { + "deregistration_delay": "120", + "health_check": { + "healthy_threshold": 5, + "interval": 15, + "path": "/test", + "protocol": "HTTP", + "unhealthy_threshold": 3 + }, + "name_prefix": "TSTAPP", + "port": 80, + "protocol": "HTTP", + "tags": { + "type": "blue" + }, + "target_type": "ip", + "vpc_id": "\${data.aws_vpc.testPocketApp_pocket_vpc_C4E157E3.id}" + } + }, + "aws_appautoscaling_policy": { + "testPocketApp_autoscaling_scale_in_policy_A163A235": { + "depends_on": [ + "aws_appautoscaling_target.testPocketApp_autoscaling_autoscaling_target_505E4EA1" + ], + "name": "testapp-ScaleInPolicy", + "policy_type": "StepScaling", + "resource_id": "\${aws_appautoscaling_target.testPocketApp_autoscaling_autoscaling_target_505E4EA1.resource_id}", + "scalable_dimension": "\${aws_appautoscaling_target.testPocketApp_autoscaling_autoscaling_target_505E4EA1.scalable_dimension}", + "service_namespace": "\${aws_appautoscaling_target.testPocketApp_autoscaling_autoscaling_target_505E4EA1.service_namespace}", + "step_scaling_policy_configuration": { + "adjustment_type": "ChangeInCapacity", + "cooldown": 60, + "metric_aggregation_type": "Average", + "step_adjustment": [ + { + "metric_interval_upper_bound": "0", + "scaling_adjustment": -1 + } + ] + }, + "target_tracking_scaling_policy_configuration": null + }, + "testPocketApp_autoscaling_scale_out_policy_075BFAC4": { + "depends_on": [ + "aws_appautoscaling_target.testPocketApp_autoscaling_autoscaling_target_505E4EA1" + ], + "name": "testapp-ScaleOutPolicy", + "policy_type": "StepScaling", + "resource_id": "\${aws_appautoscaling_target.testPocketApp_autoscaling_autoscaling_target_505E4EA1.resource_id}", + "scalable_dimension": "\${aws_appautoscaling_target.testPocketApp_autoscaling_autoscaling_target_505E4EA1.scalable_dimension}", + "service_namespace": "\${aws_appautoscaling_target.testPocketApp_autoscaling_autoscaling_target_505E4EA1.service_namespace}", + "step_scaling_policy_configuration": { + "adjustment_type": "ChangeInCapacity", + "cooldown": 60, + "metric_aggregation_type": "Average", + "step_adjustment": [ + { + "metric_interval_lower_bound": "0", + "scaling_adjustment": 2 + } + ] + }, + "target_tracking_scaling_policy_configuration": null + } + }, + "aws_appautoscaling_target": { + "testPocketApp_autoscaling_autoscaling_target_505E4EA1": { + "max_capacity": 2, + "min_capacity": 1, + "resource_id": "service/\${aws_ecs_cluster.testPocketApp_ecs_cluster_C3960066.name}/\${aws_ecs_service.testPocketApp_ecs_service_ecs-service_182DEA4C.name}", + "scalable_dimension": "ecs:service:DesiredCount", + "service_namespace": "ecs" + } + }, + "aws_cloudwatch_dashboard": { + "testPocketApp_cloudwatch-dashboard_26E9F70C": { + "dashboard_body": "{\\"widgets\\":[{\\"type\\":\\"metric\\",\\"x\\":0,\\"y\\":0,\\"width\\":12,\\"height\\":6,\\"properties\\":{\\"metrics\\":[[\\"AWS/ApplicationELB\\",\\"HTTPCode_Target_4XX_Count\\",\\"LoadBalancer\\",\\"\${aws_alb.testPocketApp_application_load_balancer_alb_D538DEEE.arn_suffix}\\",{\\"yAxis\\":\\"left\\",\\"color\\":\\"#ff7f0e\\"}],[\\".\\",\\"RequestCount\\",\\".\\",\\".\\",{\\"yAxis\\":\\"right\\",\\"color\\":\\"#1f77b4\\"}],[\\".\\",\\"HTTPCode_Target_5XX_Count\\",\\".\\",\\".\\",{\\"color\\":\\"#d62728\\"}],[\\".\\",\\"HTTPCode_Target_2XX_Count\\",\\".\\",\\".\\",{\\"yAxis\\":\\"right\\",\\"color\\":\\"#2ca02c\\"}]],\\"view\\":\\"timeSeries\\",\\"stacked\\":false,\\"region\\":\\"us-east-1\\",\\"period\\":60,\\"stat\\":\\"Sum\\",\\"title\\":\\"Target Requests\\"}},{\\"type\\":\\"metric\\",\\"x\\":12,\\"y\\":0,\\"width\\":12,\\"height\\":6,\\"properties\\":{\\"metrics\\":[[\\"AWS/ApplicationELB\\",\\"HTTPCode_ELB_4XX_Count\\",\\"LoadBalancer\\",\\"\${aws_alb.testPocketApp_application_load_balancer_alb_D538DEEE.arn_suffix}\\",{\\"yAxis\\":\\"left\\",\\"color\\":\\"#ff7f0e\\"}],[\\".\\",\\"RequestCount\\",\\".\\",\\".\\",{\\"yAxis\\":\\"right\\",\\"color\\":\\"#1f77b4\\"}],[\\".\\",\\"HTTPCode_ELB_5XX_Count\\",\\".\\",\\".\\",{\\"color\\":\\"#d62728\\"}]],\\"view\\":\\"timeSeries\\",\\"stacked\\":false,\\"region\\":\\"us-east-1\\",\\"period\\":60,\\"stat\\":\\"Sum\\",\\"title\\":\\"ALB Requests\\"}},{\\"type\\":\\"metric\\",\\"x\\":12,\\"y\\":6,\\"width\\":12,\\"height\\":6,\\"properties\\":{\\"metrics\\":[[\\"AWS/ApplicationELB\\",\\"TargetResponseTime\\",\\"LoadBalancer\\",\\"\${aws_alb.testPocketApp_application_load_balancer_alb_D538DEEE.arn_suffix}\\",{\\"label\\":\\"Average\\",\\"color\\":\\"#aec7e8\\"}],[\\"...\\",{\\"stat\\":\\"p95\\",\\"label\\":\\"p95\\",\\"color\\":\\"#ffbb78\\"}],[\\"...\\",{\\"stat\\":\\"p99\\",\\"label\\":\\"p99\\",\\"color\\":\\"#98df8a\\"}]],\\"view\\":\\"timeSeries\\",\\"stacked\\":false,\\"region\\":\\"us-east-1\\",\\"stat\\":\\"Average\\",\\"period\\":60}},{\\"type\\":\\"metric\\",\\"x\\":0,\\"y\\":6,\\"width\\":12,\\"height\\":6,\\"properties\\":{\\"metrics\\":[[\\"ECS/ContainerInsights\\",\\"RunningTaskCount\\",\\"ServiceName\\",\\"\${aws_ecs_service.testPocketApp_ecs_service_ecs-service_182DEA4C.name}\\",\\"ClusterName\\",\\"\${aws_ecs_cluster.testPocketApp_ecs_cluster_C3960066.name}\\",{\\"yAxis\\":\\"right\\",\\"color\\":\\"#c49c94\\"}],[\\"AWS/ECS\\",\\"CPUUtilization\\",\\".\\",\\".\\",\\".\\",\\".\\",{\\"color\\":\\"#f7b6d2\\"}],[\\".\\",\\"MemoryUtilization\\",\\".\\",\\".\\",\\".\\",\\".\\",{\\"color\\":\\"#c7c7c7\\"}]],\\"view\\":\\"timeSeries\\",\\"stacked\\":false,\\"region\\":\\"us-east-1\\",\\"stat\\":\\"Average\\",\\"period\\":60,\\"annotations\\":{\\"horizontal\\":[{\\"color\\":\\"#e377c2\\",\\"label\\":\\"CPU scale out\\",\\"value\\":45},{\\"color\\":\\"#c5b0d5\\",\\"label\\":\\"CPU scale in\\",\\"value\\":30}]},\\"title\\":\\"Service Load\\"}}]}", + "dashboard_name": "testapp", + "lifecycle": { + "ignore_changes": [ + "dashboard_body" + ] + } + } + }, + "aws_cloudwatch_log_group": { + "testPocketApp_ecs_service_ecs-someMountPoint_BC9739A3": { + "name_prefix": "/ecs/testapp/someMountPoint", + "retention_in_days": 30 + } + }, + "aws_cloudwatch_metric_alarm": { + "testPocketApp_autoscaling_scale_in_alarm_69B5F00D": { + "alarm_actions": [ + "\${aws_appautoscaling_policy.testPocketApp_autoscaling_scale_in_policy_A163A235.arn}" + ], + "alarm_description": "Alarm to reduce capacity if container CPU is low", + "alarm_name": "testapp Service Low CPU", + "comparison_operator": "LessThanThreshold", + "dimensions": { + "ClusterName": "\${aws_ecs_cluster.testPocketApp_ecs_cluster_C3960066.name}", + "ServiceName": "\${aws_ecs_service.testPocketApp_ecs_service_ecs-service_182DEA4C.name}" + }, + "evaluation_periods": 2, + "metric_name": "CPUUtilization", + "namespace": "AWS/ECS", + "period": 60, + "statistic": "Average", + "threshold": 30, + "treat_missing_data": "notBreaching" + }, + "testPocketApp_autoscaling_scale_out_alarm_4313FBE9": { + "alarm_actions": [ + "\${aws_appautoscaling_policy.testPocketApp_autoscaling_scale_out_policy_075BFAC4.arn}" + ], + "alarm_description": "Alarm to add capacity if container CPU is high", + "alarm_name": "testapp Service High CPU", + "comparison_operator": "GreaterThanThreshold", + "dimensions": { + "ClusterName": "\${aws_ecs_cluster.testPocketApp_ecs_cluster_C3960066.name}", + "ServiceName": "\${aws_ecs_service.testPocketApp_ecs_service_ecs-service_182DEA4C.name}" + }, + "evaluation_periods": 2, + "metric_name": "CPUUtilization", + "namespace": "AWS/ECS", + "period": 60, + "statistic": "Average", + "threshold": 45, + "treat_missing_data": "notBreaching" + } + }, + "aws_ecr_lifecycle_policy": { + "testPocketApp_ecs_service_ecr-someMountPoint_ecr-repo-lifecyclepolicy_6B94567D": { + "depends_on": [ + "aws_ecr_repository.testPocketApp_ecs_service_ecr-someMountPoint_ecr-repo_B9A958B3" + ], + "policy": "{\\"rules\\":[{\\"rulePriority\\":1,\\"description\\":\\"expire old images\\",\\"selection\\":{\\"tagStatus\\":\\"any\\",\\"countType\\":\\"imageCountMoreThan\\",\\"countNumber\\":800},\\"action\\":{\\"type\\":\\"expire\\"}}]}", + "repository": "\${aws_ecr_repository.testPocketApp_ecs_service_ecr-someMountPoint_ecr-repo_B9A958B3.name}" + } + }, + "aws_ecr_repository": { + "testPocketApp_ecs_service_ecr-someMountPoint_ecr-repo_B9A958B3": { + "image_scanning_configuration": { + "scan_on_push": true + }, + "name": "testapp-somemountpoint" + } + }, + "aws_ecs_cluster": { + "testPocketApp_ecs_cluster_C3960066": { + "name": "testapp", + "setting": [ + { + "name": "containerInsights", + "value": "enabled" + } + ] + } + }, + "aws_ecs_service": { + "testPocketApp_ecs_service_ecs-service_182DEA4C": { + "cluster": "\${aws_ecs_cluster.testPocketApp_ecs_cluster_C3960066.arn}", + "depends_on": [ + "aws_ecr_repository.testPocketApp_ecs_service_ecr-someMountPoint_ecr-repo_B9A958B3", + "aws_alb_target_group.testPocketApp_ecs_service_blue_target_group_ecs_target_group_A4B85885", + "aws_alb_listener_rule.testPocketApp_ecs_service_listener_rule_C03BB04D" + ], + "deployment_controller": { + "type": "ECS" + }, + "deployment_maximum_percent": 200, + "deployment_minimum_healthy_percent": 100, + "desired_count": 2, + "launch_type": "FARGATE", + "lifecycle": { + "ignore_changes": [ + "desired_count", + "load_balancer" + ] + }, + "load_balancer": [ + { + "container_name": "main_container", + "container_port": 8675309, + "target_group_arn": "\${aws_alb_target_group.testPocketApp_ecs_service_blue_target_group_ecs_target_group_A4B85885.arn}" + } + ], + "name": "testapp", + "network_configuration": { + "security_groups": [ + "\${aws_security_group.testPocketApp_ecs_service_ecs_security_group_9C016FA8.id}" + ], + "subnets": "\${data.aws_subnets.testPocketApp_pocket_vpc_private_subnet_ids_EB1E3A65.ids}" + }, + "propagate_tags": "SERVICE", + "task_definition": "\${aws_ecs_task_definition.testPocketApp_ecs_service_ecs-task_A7E74E45.arn}" + } + }, + "aws_ecs_task_definition": { + "testPocketApp_ecs_service_ecs-task_A7E74E45": { + "container_definitions": "[{\\"dnsSearchDomains\\":null,\\"environmentFiles\\":null,\\"logConfiguration\\":{\\"logDriver\\":\\"awslogs\\",\\"secretOptions\\":[],\\"options\\":{\\"awslogs-group\\":\\"\${aws_cloudwatch_log_group.testPocketApp_ecs_service_ecs-someMountPoint_BC9739A3.name}\\",\\"awslogs-region\\":\\"us-east-1\\",\\"awslogs-stream-prefix\\":\\"ecs\\"}},\\"entryPoint\\":null,\\"portMappings\\":[],\\"linuxParameters\\":null,\\"cpu\\":0,\\"environment\\":[],\\"resourceRequirements\\":null,\\"ulimits\\":null,\\"repositoryCredentials\\":null,\\"dnsServers\\":null,\\"mountPoints\\":[{\\"containerPath\\":\\"/qdrant/storage\\",\\"sourceVolume\\":\\"sourceVolume\\"}],\\"workingDirectory\\":null,\\"secrets\\":null,\\"dockerSecurityOptions\\":null,\\"memory\\":null,\\"memoryReservation\\":null,\\"volumesFrom\\":[],\\"stopTimeout\\":null,\\"image\\":\\"\${aws_ecr_repository.testPocketApp_ecs_service_ecr-someMountPoint_ecr-repo_B9A958B3.repository_url}:latest\\",\\"startTimeout\\":null,\\"firelensConfiguration\\":null,\\"dependsOn\\":null,\\"disableNetworking\\":null,\\"interactive\\":null,\\"healthCheck\\":null,\\"essential\\":true,\\"links\\":null,\\"hostname\\":null,\\"extraHosts\\":null,\\"pseudoTerminal\\":null,\\"user\\":null,\\"readonlyRootFilesystem\\":false,\\"dockerLabels\\":null,\\"systemControls\\":null,\\"privileged\\":null,\\"name\\":\\"someMountPoint\\"}]", + "cpu": "512", + "depends_on": [ + "aws_ecr_repository.testPocketApp_ecs_service_ecr-someMountPoint_ecr-repo_B9A958B3" + ], + "execution_role_arn": "\${aws_iam_role.testPocketApp_ecs_service_ecs-iam_ecs-execution-role_EB461A0C.arn}", + "family": "testapp", + "memory": "2048", + "network_mode": "awsvpc", + "requires_compatibilities": [ + "FARGATE" + ], + "task_role_arn": "\${aws_iam_role.testPocketApp_ecs_service_ecs-iam_ecs-task-role_7DB93963.arn}", + "volume": [ + { + "efs_volume_configuration": { + "file_system_id": "\${aws_efs_file_system.testPocketApp_efsFs_92747227.id}" + }, + "name": "sourceVolume" + } + ] + } + }, + "aws_efs_file_system": { + "testPocketApp_efsFs_92747227": { + "creation_token": "someToken", + "encrypted": true + } + }, + "aws_efs_file_system_policy": { + "testPocketApp_ecs_service_efsFsPolicy_CDE65D86": { + "depends_on": [ + "time_sleep.testPocketApp_ecs_service_waitTwoMinutes_E07A2D17" + ], + "file_system_id": "\${aws_efs_file_system.testPocketApp_efsFs_92747227.id}", + "policy": "{\\"Version\\":\\"2012-10-17\\",\\"Id\\":\\"testapp\\",\\"Statement\\":[{\\"Sid\\":\\"testapp\\",\\"Effect\\":\\"Allow\\",\\"Principal\\":{\\"AWS\\":\\"*\\"},\\"Resource\\":\\"\${aws_efs_file_system.testPocketApp_efsFs_92747227.arn}\\",\\"Action\\":[\\"elasticfilesystem:ClientMount\\",\\"elasticfilesystem:ClientWrite\\",\\"elasticfilesystem:ClientRootAccess\\"],\\"Condition\\":{\\"Bool\\":{\\"elasticfilesystem:AccessedViaMountTarget\\":\\"true\\"}}}]}" + } + }, + "aws_efs_mount_target": { + "testPocketApp_ecs_service_efs_mount_target_97B5C5E6": { + "file_system_id": "\${aws_efs_file_system.testPocketApp_efsFs_92747227.id}", + "for_each": "\${toset(data.aws_subnets.testPocketApp_pocket_vpc_private_subnet_ids_EB1E3A65.ids)}", + "security_groups": [ + "\${aws_security_group.testPocketApp_ecs_service_efs_mount_sg_01974774.id}" + ], + "subnet_id": "\${each.value}" + } + }, + "aws_iam_role": { + "testPocketApp_ecs_service_ecs-iam_ecs-execution-role_EB461A0C": { + "assume_role_policy": "\${data.aws_iam_policy_document.testPocketApp_ecs_service_ecs-iam_ecs-task-assume_ABB81680.json}", + "name": "testapp-TaskExecutionRole" + }, + "testPocketApp_ecs_service_ecs-iam_ecs-task-role_7DB93963": { + "assume_role_policy": "\${data.aws_iam_policy_document.testPocketApp_ecs_service_ecs-iam_ecs-task-assume_ABB81680.json}", + "name": "testapp-TaskRole" + } + }, + "aws_route53_record": { + "testPocketApp_alb_certificate_certificate_record_3C49201F": { + "depends_on": [ + "aws_acm_certificate.testPocketApp_alb_certificate_417C14FF" + ], + "name": "\${tolist(aws_acm_certificate.testPocketApp_alb_certificate_417C14FF.domain_validation_options).0.resource_record_name}", + "records": [ + "\${tolist(aws_acm_certificate.testPocketApp_alb_certificate_417C14FF.domain_validation_options).0.resource_record_value}" + ], + "ttl": 60, + "type": "\${tolist(aws_acm_certificate.testPocketApp_alb_certificate_417C14FF.domain_validation_options).0.resource_record_type}", + "zone_id": "\${aws_route53_zone.testPocketApp_base_dns_subhosted_zone_1E1E3243.zone_id}" + }, + "testPocketApp_alb_record_7B56ED33": { + "alias": { + "evaluate_target_health": true, + "name": "\${aws_alb.testPocketApp_application_load_balancer_alb_D538DEEE.dns_name}", + "zone_id": "\${aws_alb.testPocketApp_application_load_balancer_alb_D538DEEE.zone_id}" + }, + "lifecycle": { + "ignore_changes": [ + "weighted_routing_policy[0].weight" + ] + }, + "name": "testing.bowling.gov", + "set_identifier": "1", + "type": "A", + "weighted_routing_policy": { + "weight": 1 + }, + "zone_id": "\${aws_route53_zone.testPocketApp_base_dns_subhosted_zone_1E1E3243.zone_id}" + }, + "testPocketApp_base_dns_subhosted_zone_ns_497FFB83": { + "name": "testing.bowling.gov", + "records": "\${aws_route53_zone.testPocketApp_base_dns_subhosted_zone_1E1E3243.name_servers}", + "ttl": 86400, + "type": "NS", + "zone_id": "\${data.aws_route53_zone.testPocketApp_base_dns_main_hosted_zone_9442EEB0.zone_id}" + } + }, + "aws_route53_zone": { + "testPocketApp_base_dns_subhosted_zone_1E1E3243": { + "name": "testing.bowling.gov" + } + }, + "aws_security_group": { + "testPocketApp_application_load_balancer_alb_security_group_91A27046": { + "description": "External security group (Managed by Terraform)", + "egress": [ + { + "cidr_blocks": [ + "0.0.0.0/0" + ], + "description": "required", + "from_port": 0, + "ipv6_cidr_blocks": [ + ], + "prefix_list_ids": [ + ], + "protocol": "-1", + "security_groups": [ + ], + "self": null, + "to_port": 0 + } + ], + "ingress": [ + { + "cidr_blocks": [ + "0.0.0.0/0" + ], + "description": null, + "from_port": 443, + "ipv6_cidr_blocks": null, + "prefix_list_ids": null, + "protocol": "TCP", + "security_groups": null, + "self": null, + "to_port": 443 + }, + { + "cidr_blocks": [ + "0.0.0.0/0" + ], + "description": null, + "from_port": 80, + "ipv6_cidr_blocks": null, + "prefix_list_ids": null, + "protocol": "TCP", + "security_groups": null, + "self": null, + "to_port": 80 + } + ], + "lifecycle": { + "create_before_destroy": true + }, + "name_prefix": "testapp-HTTP/S Security Group", + "tags": { + "Name": "testapp-HTTP/S Security Group" + }, + "vpc_id": "\${data.aws_vpc.testPocketApp_pocket_vpc_C4E157E3.id}" + }, + "testPocketApp_ecs_service_ecs_security_group_9C016FA8": { + "description": "Internal ECS Security Group (Managed by Terraform)", + "egress": [ + { + "cidr_blocks": [ + "0.0.0.0/0" + ], + "description": "required", + "from_port": 0, + "ipv6_cidr_blocks": [ + ], + "prefix_list_ids": [ + ], + "protocol": "-1", + "security_groups": [ + ], + "self": null, + "to_port": 0 + } + ], + "ingress": [ + { + "cidr_blocks": [ + ], + "description": "required", + "from_port": 80, + "ipv6_cidr_blocks": [ + ], + "prefix_list_ids": [ + ], + "protocol": "TCP", + "security_groups": [ + "\${aws_security_group.testPocketApp_application_load_balancer_alb_security_group_91A27046.id}" + ], + "self": null, + "to_port": 8675309 + } + ], + "lifecycle": { + "create_before_destroy": true + }, + "name_prefix": "testapp-ECSSecurityGroup", + "vpc_id": "\${data.aws_vpc.testPocketApp_pocket_vpc_C4E157E3.id}" + }, + "testPocketApp_ecs_service_efs_mount_sg_01974774": { + "description": "ECS EFS Mount (Managed by Terraform)", + "egress": [ + { + "cidr_blocks": [ + "0.0.0.0/0" + ], + "description": "required", + "from_port": 0, + "ipv6_cidr_blocks": [ + ], + "prefix_list_ids": [ + ], + "protocol": "-1", + "security_groups": [ + ], + "self": null, + "to_port": 0 + } + ], + "ingress": [ + { + "cidr_blocks": [ + ], + "description": "required", + "from_port": 2049, + "ipv6_cidr_blocks": [ + ], + "prefix_list_ids": [ + ], + "protocol": "TCP", + "security_groups": [ + "\${aws_security_group.testPocketApp_ecs_service_ecs_security_group_9C016FA8.id}" + ], + "self": null, + "to_port": 2049 + } + ], + "lifecycle": { + "create_before_destroy": true + }, + "name_prefix": "testapp-ECSSMountPoint", + "vpc_id": "\${data.aws_vpc.testPocketApp_pocket_vpc_C4E157E3.id}" + } + }, + "time_sleep": { + "testPocketApp_ecs_service_waitTwoMinutes_E07A2D17": { + "create_duration": "2m", + "depends_on": [ + ] + } + } + } +}" +`; + +exports[`PocketALBApplication renders a Pocket App with attached waf 1`] = ` +"{ + "data": { + "aws_caller_identity": { + "testPocketApp_pocket_vpc_current_identity_06D87057": { + } + }, + "aws_iam_policy_document": { + "testPocketApp_ecs_service_ecs-iam_ecs-task-assume_ABB81680": { + "statement": [ + { + "actions": [ + "sts:AssumeRole" + ], + "effect": "Allow", + "principals": [ + { + "identifiers": [ + "ecs-tasks.amazonaws.com" + ], + "type": "Service" + } + ] + } + ], + "version": "2012-10-17" + } + }, + "aws_kms_alias": { + "testPocketApp_pocket_vpc_secrets_manager_key_9FD2BC93": { + "name": "alias/aws/secretsmanager" + } + }, + "aws_region": { + "testPocketApp_pocket_vpc_current_region_8ED435E7": { + } + }, + "aws_route53_zone": { + "testPocketApp_base_dns_main_hosted_zone_9442EEB0": { + "name": "bowling.gov" + } + }, + "aws_security_groups": { + "testPocketApp_pocket_vpc_default_security_groups_40B4FC48": { + "filter": [ + { + "name": "group-name", + "values": [ + "default" + ] + }, + { + "name": "vpc-id", + "values": [ + "\${data.aws_vpc.testPocketApp_pocket_vpc_C4E157E3.id}" + ] + } + ] + }, + "testPocketApp_pocket_vpc_internal_security_groups_12F7F666": { + "filter": [ + { + "name": "group-name", + "values": [ + "pocket-vpc-internal" + ] + }, + { + "name": "vpc-id", + "values": [ + "\${data.aws_vpc.testPocketApp_pocket_vpc_C4E157E3.id}" + ] + } + ] + } + }, + "aws_ssm_parameter": { + "testPocketApp_pocket_vpc_private_subnets_9D449563": { + "name": "/Shared/PrivateSubnets" + }, + "testPocketApp_pocket_vpc_public_subnets_B0B3A6AB": { + "name": "/Shared/PublicSubnets" + }, + "testPocketApp_pocket_vpc_vpc_ssm_param_235525D4": { + "name": "/Shared/Vpc" + } + }, + "aws_subnets": { + "testPocketApp_pocket_vpc_private_subnet_ids_EB1E3A65": { + "filter": [ + { + "name": "subnet-id", + "values": "\${split(\\",\\", data.aws_ssm_parameter.testPocketApp_pocket_vpc_private_subnets_9D449563.value)}" + }, + { + "name": "vpc-id", + "values": [ + "\${data.aws_vpc.testPocketApp_pocket_vpc_C4E157E3.id}" + ] + } + ] + }, + "testPocketApp_pocket_vpc_public_subnet_ids_01F0B902": { + "filter": [ + { + "name": "subnet-id", + "values": "\${split(\\",\\", data.aws_ssm_parameter.testPocketApp_pocket_vpc_public_subnets_B0B3A6AB.value)}" + }, + { + "name": "vpc-id", + "values": [ + "\${data.aws_vpc.testPocketApp_pocket_vpc_C4E157E3.id}" + ] + } + ] + } + }, + "aws_vpc": { + "testPocketApp_pocket_vpc_C4E157E3": { + "filter": [ + { + "name": "vpc-id", + "values": [ + "\${data.aws_ssm_parameter.testPocketApp_pocket_vpc_vpc_ssm_param_235525D4.value}" + ] + } + ] + } + } + }, + "resource": { + "aws_acm_certificate": { + "testPocketApp_alb_certificate_417C14FF": { + "domain_name": "testing.bowling.gov", + "lifecycle": { + "create_before_destroy": true + }, + "validation_method": "DNS" + } + }, + "aws_acm_certificate_validation": { + "testPocketApp_alb_certificate_certificate_validation_7D899C6A": { + "certificate_arn": "\${aws_acm_certificate.testPocketApp_alb_certificate_417C14FF.arn}", + "depends_on": [ + "aws_route53_record.testPocketApp_alb_certificate_certificate_record_3C49201F", + "aws_acm_certificate.testPocketApp_alb_certificate_417C14FF" + ], + "validation_record_fqdns": [ + "\${aws_route53_record.testPocketApp_alb_certificate_certificate_record_3C49201F.fqdn}" + ] + } + }, + "aws_alb": { + "testPocketApp_application_load_balancer_alb_D538DEEE": { + "internal": false, + "name_prefix": "TSTAPP", + "security_groups": [ + "\${aws_security_group.testPocketApp_application_load_balancer_alb_security_group_91A27046.id}" + ], + "subnets": "\${data.aws_subnets.testPocketApp_pocket_vpc_public_subnet_ids_01F0B902.ids}" + } + }, + "aws_alb_listener": { + "testPocketApp_listener_http_A9E227C2": { + "default_action": [ + { + "redirect": { + "port": "443", + "protocol": "HTTPS", + "status_code": "HTTP_301" + }, + "type": "redirect" + } + ], + "load_balancer_arn": "\${aws_alb.testPocketApp_application_load_balancer_alb_D538DEEE.arn}", + "port": 80, + "protocol": "HTTP" + }, + "testPocketApp_listener_https_7F49DE83": { + "certificate_arn": "\${aws_acm_certificate.testPocketApp_alb_certificate_417C14FF.arn}", + "default_action": [ + { + "fixed_response": { + "content_type": "text/plain", + "message_body": "", + "status_code": "503" + }, + "type": "fixed-response" + } + ], + "load_balancer_arn": "\${aws_alb.testPocketApp_application_load_balancer_alb_D538DEEE.arn}", + "port": 443, + "protocol": "HTTPS", + "ssl_policy": "ELBSecurityPolicy-TLS-1-1-2017-01" + } + }, + "aws_alb_listener_rule": { + "testPocketApp_ecs_service_listener_rule_C03BB04D": { + "action": [ + { + "target_group_arn": "\${aws_alb_target_group.testPocketApp_ecs_service_blue_target_group_ecs_target_group_A4B85885.arn}", + "type": "forward" + } + ], + "condition": [ + { + "path_pattern": { + "values": [ + "*" + ] + } + } + ], + "lifecycle": { + "ignore_changes": [ + "action" + ] + }, + "listener_arn": "\${aws_alb_listener.testPocketApp_listener_https_7F49DE83.arn}", + "priority": 1 + } + }, + "aws_alb_target_group": { + "testPocketApp_ecs_service_blue_target_group_ecs_target_group_A4B85885": { + "deregistration_delay": "120", + "health_check": { + "healthy_threshold": 5, + "interval": 15, + "path": "/test", + "protocol": "HTTP", + "unhealthy_threshold": 3 + }, + "name_prefix": "TSTAPP", + "port": 80, + "protocol": "HTTP", + "tags": { + "type": "blue" + }, + "target_type": "ip", + "vpc_id": "\${data.aws_vpc.testPocketApp_pocket_vpc_C4E157E3.id}" + } + }, + "aws_appautoscaling_policy": { + "testPocketApp_autoscaling_scale_in_policy_A163A235": { + "depends_on": [ + "aws_appautoscaling_target.testPocketApp_autoscaling_autoscaling_target_505E4EA1" + ], + "name": "testapp-ScaleInPolicy", + "policy_type": "StepScaling", + "resource_id": "\${aws_appautoscaling_target.testPocketApp_autoscaling_autoscaling_target_505E4EA1.resource_id}", + "scalable_dimension": "\${aws_appautoscaling_target.testPocketApp_autoscaling_autoscaling_target_505E4EA1.scalable_dimension}", + "service_namespace": "\${aws_appautoscaling_target.testPocketApp_autoscaling_autoscaling_target_505E4EA1.service_namespace}", + "step_scaling_policy_configuration": { + "adjustment_type": "ChangeInCapacity", + "cooldown": 60, + "metric_aggregation_type": "Average", + "step_adjustment": [ + { + "metric_interval_upper_bound": "0", + "scaling_adjustment": -1 + } + ] + }, + "target_tracking_scaling_policy_configuration": null + }, + "testPocketApp_autoscaling_scale_out_policy_075BFAC4": { + "depends_on": [ + "aws_appautoscaling_target.testPocketApp_autoscaling_autoscaling_target_505E4EA1" + ], + "name": "testapp-ScaleOutPolicy", + "policy_type": "StepScaling", + "resource_id": "\${aws_appautoscaling_target.testPocketApp_autoscaling_autoscaling_target_505E4EA1.resource_id}", + "scalable_dimension": "\${aws_appautoscaling_target.testPocketApp_autoscaling_autoscaling_target_505E4EA1.scalable_dimension}", + "service_namespace": "\${aws_appautoscaling_target.testPocketApp_autoscaling_autoscaling_target_505E4EA1.service_namespace}", + "step_scaling_policy_configuration": { + "adjustment_type": "ChangeInCapacity", + "cooldown": 60, + "metric_aggregation_type": "Average", + "step_adjustment": [ + { + "metric_interval_lower_bound": "0", + "scaling_adjustment": 2 + } + ] + }, + "target_tracking_scaling_policy_configuration": null + } + }, + "aws_appautoscaling_target": { + "testPocketApp_autoscaling_autoscaling_target_505E4EA1": { + "max_capacity": 2, + "min_capacity": 1, + "resource_id": "service/\${aws_ecs_cluster.testPocketApp_ecs_cluster_C3960066.name}/\${aws_ecs_service.testPocketApp_ecs_service_ecs-service_182DEA4C.name}", + "scalable_dimension": "ecs:service:DesiredCount", + "service_namespace": "ecs" + } + }, + "aws_cloudwatch_dashboard": { + "testPocketApp_cloudwatch-dashboard_26E9F70C": { + "dashboard_body": "{\\"widgets\\":[{\\"type\\":\\"metric\\",\\"x\\":0,\\"y\\":0,\\"width\\":12,\\"height\\":6,\\"properties\\":{\\"metrics\\":[[\\"AWS/ApplicationELB\\",\\"HTTPCode_Target_4XX_Count\\",\\"LoadBalancer\\",\\"\${aws_alb.testPocketApp_application_load_balancer_alb_D538DEEE.arn_suffix}\\",{\\"yAxis\\":\\"left\\",\\"color\\":\\"#ff7f0e\\"}],[\\".\\",\\"RequestCount\\",\\".\\",\\".\\",{\\"yAxis\\":\\"right\\",\\"color\\":\\"#1f77b4\\"}],[\\".\\",\\"HTTPCode_Target_5XX_Count\\",\\".\\",\\".\\",{\\"color\\":\\"#d62728\\"}],[\\".\\",\\"HTTPCode_Target_2XX_Count\\",\\".\\",\\".\\",{\\"yAxis\\":\\"right\\",\\"color\\":\\"#2ca02c\\"}]],\\"view\\":\\"timeSeries\\",\\"stacked\\":false,\\"region\\":\\"us-east-1\\",\\"period\\":60,\\"stat\\":\\"Sum\\",\\"title\\":\\"Target Requests\\"}},{\\"type\\":\\"metric\\",\\"x\\":12,\\"y\\":0,\\"width\\":12,\\"height\\":6,\\"properties\\":{\\"metrics\\":[[\\"AWS/ApplicationELB\\",\\"HTTPCode_ELB_4XX_Count\\",\\"LoadBalancer\\",\\"\${aws_alb.testPocketApp_application_load_balancer_alb_D538DEEE.arn_suffix}\\",{\\"yAxis\\":\\"left\\",\\"color\\":\\"#ff7f0e\\"}],[\\".\\",\\"RequestCount\\",\\".\\",\\".\\",{\\"yAxis\\":\\"right\\",\\"color\\":\\"#1f77b4\\"}],[\\".\\",\\"HTTPCode_ELB_5XX_Count\\",\\".\\",\\".\\",{\\"color\\":\\"#d62728\\"}]],\\"view\\":\\"timeSeries\\",\\"stacked\\":false,\\"region\\":\\"us-east-1\\",\\"period\\":60,\\"stat\\":\\"Sum\\",\\"title\\":\\"ALB Requests\\"}},{\\"type\\":\\"metric\\",\\"x\\":12,\\"y\\":6,\\"width\\":12,\\"height\\":6,\\"properties\\":{\\"metrics\\":[[\\"AWS/ApplicationELB\\",\\"TargetResponseTime\\",\\"LoadBalancer\\",\\"\${aws_alb.testPocketApp_application_load_balancer_alb_D538DEEE.arn_suffix}\\",{\\"label\\":\\"Average\\",\\"color\\":\\"#aec7e8\\"}],[\\"...\\",{\\"stat\\":\\"p95\\",\\"label\\":\\"p95\\",\\"color\\":\\"#ffbb78\\"}],[\\"...\\",{\\"stat\\":\\"p99\\",\\"label\\":\\"p99\\",\\"color\\":\\"#98df8a\\"}]],\\"view\\":\\"timeSeries\\",\\"stacked\\":false,\\"region\\":\\"us-east-1\\",\\"stat\\":\\"Average\\",\\"period\\":60}},{\\"type\\":\\"metric\\",\\"x\\":0,\\"y\\":6,\\"width\\":12,\\"height\\":6,\\"properties\\":{\\"metrics\\":[[\\"ECS/ContainerInsights\\",\\"RunningTaskCount\\",\\"ServiceName\\",\\"\${aws_ecs_service.testPocketApp_ecs_service_ecs-service_182DEA4C.name}\\",\\"ClusterName\\",\\"\${aws_ecs_cluster.testPocketApp_ecs_cluster_C3960066.name}\\",{\\"yAxis\\":\\"right\\",\\"color\\":\\"#c49c94\\"}],[\\"AWS/ECS\\",\\"CPUUtilization\\",\\".\\",\\".\\",\\".\\",\\".\\",{\\"color\\":\\"#f7b6d2\\"}],[\\".\\",\\"MemoryUtilization\\",\\".\\",\\".\\",\\".\\",\\".\\",{\\"color\\":\\"#c7c7c7\\"}]],\\"view\\":\\"timeSeries\\",\\"stacked\\":false,\\"region\\":\\"us-east-1\\",\\"stat\\":\\"Average\\",\\"period\\":60,\\"annotations\\":{\\"horizontal\\":[{\\"color\\":\\"#e377c2\\",\\"label\\":\\"CPU scale out\\",\\"value\\":45},{\\"color\\":\\"#c5b0d5\\",\\"label\\":\\"CPU scale in\\",\\"value\\":30}]},\\"title\\":\\"Service Load\\"}}]}", + "dashboard_name": "testapp", + "lifecycle": { + "ignore_changes": [ + "dashboard_body" + ] + } + } + }, + "aws_cloudwatch_metric_alarm": { + "testPocketApp_autoscaling_scale_in_alarm_69B5F00D": { + "alarm_actions": [ + "\${aws_appautoscaling_policy.testPocketApp_autoscaling_scale_in_policy_A163A235.arn}" + ], + "alarm_description": "Alarm to reduce capacity if container CPU is low", + "alarm_name": "testapp Service Low CPU", + "comparison_operator": "LessThanThreshold", + "dimensions": { + "ClusterName": "\${aws_ecs_cluster.testPocketApp_ecs_cluster_C3960066.name}", + "ServiceName": "\${aws_ecs_service.testPocketApp_ecs_service_ecs-service_182DEA4C.name}" + }, + "evaluation_periods": 2, + "metric_name": "CPUUtilization", + "namespace": "AWS/ECS", + "period": 60, + "statistic": "Average", + "threshold": 30, + "treat_missing_data": "notBreaching" + }, + "testPocketApp_autoscaling_scale_out_alarm_4313FBE9": { + "alarm_actions": [ + "\${aws_appautoscaling_policy.testPocketApp_autoscaling_scale_out_policy_075BFAC4.arn}" + ], + "alarm_description": "Alarm to add capacity if container CPU is high", + "alarm_name": "testapp Service High CPU", + "comparison_operator": "GreaterThanThreshold", + "dimensions": { + "ClusterName": "\${aws_ecs_cluster.testPocketApp_ecs_cluster_C3960066.name}", + "ServiceName": "\${aws_ecs_service.testPocketApp_ecs_service_ecs-service_182DEA4C.name}" + }, + "evaluation_periods": 2, + "metric_name": "CPUUtilization", + "namespace": "AWS/ECS", + "period": 60, + "statistic": "Average", + "threshold": 45, + "treat_missing_data": "notBreaching" + } + }, + "aws_ecs_cluster": { + "testPocketApp_ecs_cluster_C3960066": { + "name": "testapp", + "setting": [ + { + "name": "containerInsights", + "value": "enabled" + } + ] + } + }, + "aws_ecs_service": { + "testPocketApp_ecs_service_ecs-service_182DEA4C": { + "cluster": "\${aws_ecs_cluster.testPocketApp_ecs_cluster_C3960066.arn}", + "depends_on": [ + "aws_alb_target_group.testPocketApp_ecs_service_blue_target_group_ecs_target_group_A4B85885", + "aws_alb_listener_rule.testPocketApp_ecs_service_listener_rule_C03BB04D" + ], + "deployment_controller": { + "type": "ECS" + }, + "deployment_maximum_percent": 200, + "deployment_minimum_healthy_percent": 100, + "desired_count": 2, + "launch_type": "FARGATE", + "lifecycle": { + "ignore_changes": [ + "desired_count", + "load_balancer" + ] + }, + "load_balancer": [ + { + "container_name": "main_container", + "container_port": 8675309, + "target_group_arn": "\${aws_alb_target_group.testPocketApp_ecs_service_blue_target_group_ecs_target_group_A4B85885.arn}" + } + ], + "name": "testapp", + "network_configuration": { + "security_groups": [ + "\${aws_security_group.testPocketApp_ecs_service_ecs_security_group_9C016FA8.id}" + ], + "subnets": "\${data.aws_subnets.testPocketApp_pocket_vpc_private_subnet_ids_EB1E3A65.ids}" + }, + "propagate_tags": "SERVICE", + "task_definition": "\${aws_ecs_task_definition.testPocketApp_ecs_service_ecs-task_A7E74E45.arn}" + } + }, + "aws_ecs_task_definition": { + "testPocketApp_ecs_service_ecs-task_A7E74E45": { + "container_definitions": "[]", + "cpu": "512", + "depends_on": [ + ], + "execution_role_arn": "\${aws_iam_role.testPocketApp_ecs_service_ecs-iam_ecs-execution-role_EB461A0C.arn}", + "family": "testapp", + "memory": "2048", + "network_mode": "awsvpc", + "requires_compatibilities": [ + "FARGATE" + ], + "task_role_arn": "\${aws_iam_role.testPocketApp_ecs_service_ecs-iam_ecs-task-role_7DB93963.arn}", + "volume": [ + ] + } + }, + "aws_iam_role": { + "testPocketApp_ecs_service_ecs-iam_ecs-execution-role_EB461A0C": { + "assume_role_policy": "\${data.aws_iam_policy_document.testPocketApp_ecs_service_ecs-iam_ecs-task-assume_ABB81680.json}", + "name": "testapp-TaskExecutionRole" + }, + "testPocketApp_ecs_service_ecs-iam_ecs-task-role_7DB93963": { + "assume_role_policy": "\${data.aws_iam_policy_document.testPocketApp_ecs_service_ecs-iam_ecs-task-assume_ABB81680.json}", + "name": "testapp-TaskRole" + } + }, + "aws_route53_record": { + "testPocketApp_alb_certificate_certificate_record_3C49201F": { + "depends_on": [ + "aws_acm_certificate.testPocketApp_alb_certificate_417C14FF" + ], + "name": "\${tolist(aws_acm_certificate.testPocketApp_alb_certificate_417C14FF.domain_validation_options).0.resource_record_name}", + "records": [ + "\${tolist(aws_acm_certificate.testPocketApp_alb_certificate_417C14FF.domain_validation_options).0.resource_record_value}" + ], + "ttl": 60, + "type": "\${tolist(aws_acm_certificate.testPocketApp_alb_certificate_417C14FF.domain_validation_options).0.resource_record_type}", + "zone_id": "\${aws_route53_zone.testPocketApp_base_dns_subhosted_zone_1E1E3243.zone_id}" + }, + "testPocketApp_alb_record_7B56ED33": { + "alias": { + "evaluate_target_health": true, + "name": "\${aws_alb.testPocketApp_application_load_balancer_alb_D538DEEE.dns_name}", + "zone_id": "\${aws_alb.testPocketApp_application_load_balancer_alb_D538DEEE.zone_id}" + }, + "lifecycle": { + "ignore_changes": [ + "weighted_routing_policy[0].weight" + ] + }, + "name": "testing.bowling.gov", + "set_identifier": "1", + "type": "A", + "weighted_routing_policy": { + "weight": 1 + }, + "zone_id": "\${aws_route53_zone.testPocketApp_base_dns_subhosted_zone_1E1E3243.zone_id}" + }, + "testPocketApp_base_dns_subhosted_zone_ns_497FFB83": { + "name": "testing.bowling.gov", + "records": "\${aws_route53_zone.testPocketApp_base_dns_subhosted_zone_1E1E3243.name_servers}", + "ttl": 86400, + "type": "NS", + "zone_id": "\${data.aws_route53_zone.testPocketApp_base_dns_main_hosted_zone_9442EEB0.zone_id}" + } + }, + "aws_route53_zone": { + "testPocketApp_base_dns_subhosted_zone_1E1E3243": { + "name": "testing.bowling.gov" + } + }, + "aws_security_group": { + "testPocketApp_application_load_balancer_alb_security_group_91A27046": { + "description": "External security group (Managed by Terraform)", + "egress": [ + { + "cidr_blocks": [ + "0.0.0.0/0" + ], + "description": "required", + "from_port": 0, + "ipv6_cidr_blocks": [ + ], + "prefix_list_ids": [ + ], + "protocol": "-1", + "security_groups": [ + ], + "self": null, + "to_port": 0 + } + ], + "ingress": [ + { + "cidr_blocks": [ + "0.0.0.0/0" + ], + "description": null, + "from_port": 443, + "ipv6_cidr_blocks": null, + "prefix_list_ids": null, + "protocol": "TCP", + "security_groups": null, + "self": null, + "to_port": 443 + }, + { + "cidr_blocks": [ + "0.0.0.0/0" + ], + "description": null, + "from_port": 80, + "ipv6_cidr_blocks": null, + "prefix_list_ids": null, + "protocol": "TCP", + "security_groups": null, + "self": null, + "to_port": 80 + } + ], + "lifecycle": { + "create_before_destroy": true + }, + "name_prefix": "testapp-HTTP/S Security Group", + "tags": { + "Name": "testapp-HTTP/S Security Group" + }, + "vpc_id": "\${data.aws_vpc.testPocketApp_pocket_vpc_C4E157E3.id}" + }, + "testPocketApp_ecs_service_ecs_security_group_9C016FA8": { + "description": "Internal ECS Security Group (Managed by Terraform)", + "egress": [ + { + "cidr_blocks": [ + "0.0.0.0/0" + ], + "description": "required", + "from_port": 0, + "ipv6_cidr_blocks": [ + ], + "prefix_list_ids": [ + ], + "protocol": "-1", + "security_groups": [ + ], + "self": null, + "to_port": 0 + } + ], + "ingress": [ + { + "cidr_blocks": [ + ], + "description": "required", + "from_port": 80, + "ipv6_cidr_blocks": [ + ], + "prefix_list_ids": [ + ], + "protocol": "TCP", + "security_groups": [ + "\${aws_security_group.testPocketApp_application_load_balancer_alb_security_group_91A27046.id}" + ], + "self": null, + "to_port": 8675309 + } + ], + "lifecycle": { + "create_before_destroy": true + }, + "name_prefix": "testapp-ECSSecurityGroup", + "vpc_id": "\${data.aws_vpc.testPocketApp_pocket_vpc_C4E157E3.id}" + } + }, + "aws_wafv2_web_acl_association": { + "testPocketApp_application_waf_association_03F7C3FB": { + "resource_arn": "\${aws_alb.testPocketApp_application_load_balancer_alb_D538DEEE.arn}", + "web_acl_arn": "justAString" + } + } + } +}" +`; + +exports[`PocketALBApplication renders an Pocket App with code deploy 1`] = ` +"{ + "data": { + "aws_caller_identity": { + "testPocketApp_pocket_vpc_current_identity_06D87057": { + } + }, + "aws_iam_policy_document": { + "testPocketApp_ecs_service_ecs-iam_ecs-task-assume_ABB81680": { + "statement": [ + { + "actions": [ + "sts:AssumeRole" + ], + "effect": "Allow", + "principals": [ + { + "identifiers": [ + "ecs-tasks.amazonaws.com" + ], + "type": "Service" + } + ] + } + ], + "version": "2012-10-17" + }, + "testPocketApp_ecs_service_ecs_codedeploy_codedeploy_assume_role_840D1ECF": { + "statement": [ + { + "actions": [ + "sts:AssumeRole" + ], + "effect": "Allow", + "principals": [ + { + "identifiers": [ + "codedeploy.amazonaws.com" + ], + "type": "Service" + } + ] + } + ] + } + }, + "aws_kms_alias": { + "testPocketApp_pocket_vpc_secrets_manager_key_9FD2BC93": { + "name": "alias/aws/secretsmanager" + } + }, + "aws_region": { + "testPocketApp_pocket_vpc_current_region_8ED435E7": { + } + }, + "aws_route53_zone": { + "testPocketApp_base_dns_main_hosted_zone_9442EEB0": { + "name": "bowling.gov" + } + }, + "aws_security_groups": { + "testPocketApp_pocket_vpc_default_security_groups_40B4FC48": { + "filter": [ + { + "name": "group-name", + "values": [ + "default" + ] + }, + { + "name": "vpc-id", + "values": [ + "\${data.aws_vpc.testPocketApp_pocket_vpc_C4E157E3.id}" + ] + } + ] + }, + "testPocketApp_pocket_vpc_internal_security_groups_12F7F666": { + "filter": [ + { + "name": "group-name", + "values": [ + "pocket-vpc-internal" + ] + }, + { + "name": "vpc-id", + "values": [ + "\${data.aws_vpc.testPocketApp_pocket_vpc_C4E157E3.id}" + ] + } + ] + } + }, + "aws_ssm_parameter": { + "testPocketApp_pocket_vpc_private_subnets_9D449563": { + "name": "/Shared/PrivateSubnets" + }, + "testPocketApp_pocket_vpc_public_subnets_B0B3A6AB": { + "name": "/Shared/PublicSubnets" + }, + "testPocketApp_pocket_vpc_vpc_ssm_param_235525D4": { + "name": "/Shared/Vpc" + } + }, + "aws_subnets": { + "testPocketApp_pocket_vpc_private_subnet_ids_EB1E3A65": { + "filter": [ + { + "name": "subnet-id", + "values": "\${split(\\",\\", data.aws_ssm_parameter.testPocketApp_pocket_vpc_private_subnets_9D449563.value)}" + }, + { + "name": "vpc-id", + "values": [ + "\${data.aws_vpc.testPocketApp_pocket_vpc_C4E157E3.id}" + ] + } + ] + }, + "testPocketApp_pocket_vpc_public_subnet_ids_01F0B902": { + "filter": [ + { + "name": "subnet-id", + "values": "\${split(\\",\\", data.aws_ssm_parameter.testPocketApp_pocket_vpc_public_subnets_B0B3A6AB.value)}" + }, + { + "name": "vpc-id", + "values": [ + "\${data.aws_vpc.testPocketApp_pocket_vpc_C4E157E3.id}" + ] + } + ] + } + }, + "aws_vpc": { + "testPocketApp_pocket_vpc_C4E157E3": { + "filter": [ + { + "name": "vpc-id", + "values": [ + "\${data.aws_ssm_parameter.testPocketApp_pocket_vpc_vpc_ssm_param_235525D4.value}" + ] + } + ] + } + } + }, + "resource": { + "aws_acm_certificate": { + "testPocketApp_alb_certificate_417C14FF": { + "domain_name": "testing.bowling.gov", + "lifecycle": { + "create_before_destroy": true + }, + "validation_method": "DNS" + } + }, + "aws_acm_certificate_validation": { + "testPocketApp_alb_certificate_certificate_validation_7D899C6A": { + "certificate_arn": "\${aws_acm_certificate.testPocketApp_alb_certificate_417C14FF.arn}", + "depends_on": [ + "aws_route53_record.testPocketApp_alb_certificate_certificate_record_3C49201F", + "aws_acm_certificate.testPocketApp_alb_certificate_417C14FF" + ], + "validation_record_fqdns": [ + "\${aws_route53_record.testPocketApp_alb_certificate_certificate_record_3C49201F.fqdn}" + ] + } + }, + "aws_alb": { + "testPocketApp_application_load_balancer_alb_D538DEEE": { + "internal": false, + "name_prefix": "TSTAPP", + "security_groups": [ + "\${aws_security_group.testPocketApp_application_load_balancer_alb_security_group_91A27046.id}" + ], + "subnets": "\${data.aws_subnets.testPocketApp_pocket_vpc_public_subnet_ids_01F0B902.ids}" + } + }, + "aws_alb_listener": { + "testPocketApp_listener_http_A9E227C2": { + "default_action": [ + { + "redirect": { + "port": "443", + "protocol": "HTTPS", + "status_code": "HTTP_301" + }, + "type": "redirect" + } + ], + "load_balancer_arn": "\${aws_alb.testPocketApp_application_load_balancer_alb_D538DEEE.arn}", + "port": 80, + "protocol": "HTTP" + }, + "testPocketApp_listener_https_7F49DE83": { + "certificate_arn": "\${aws_acm_certificate.testPocketApp_alb_certificate_417C14FF.arn}", + "default_action": [ + { + "fixed_response": { + "content_type": "text/plain", + "message_body": "", + "status_code": "503" + }, + "type": "fixed-response" + } + ], + "load_balancer_arn": "\${aws_alb.testPocketApp_application_load_balancer_alb_D538DEEE.arn}", + "port": 443, + "protocol": "HTTPS", + "ssl_policy": "ELBSecurityPolicy-TLS-1-1-2017-01" + } + }, + "aws_alb_listener_rule": { + "testPocketApp_ecs_service_listener_rule_C03BB04D": { + "action": [ + { + "target_group_arn": "\${aws_alb_target_group.testPocketApp_ecs_service_blue_target_group_ecs_target_group_A4B85885.arn}", + "type": "forward" + } + ], + "condition": [ + { + "path_pattern": { + "values": [ + "*" + ] + } + } + ], + "lifecycle": { + "ignore_changes": [ + "action" + ] + }, + "listener_arn": "\${aws_alb_listener.testPocketApp_listener_https_7F49DE83.arn}", + "priority": 1 + } + }, + "aws_alb_target_group": { + "testPocketApp_ecs_service_blue_target_group_ecs_target_group_A4B85885": { + "deregistration_delay": "120", + "health_check": { + "healthy_threshold": 5, + "interval": 15, + "path": "/test", + "protocol": "HTTP", + "unhealthy_threshold": 3 + }, + "name_prefix": "TSTAPP", + "port": 80, + "protocol": "HTTP", + "tags": { + "type": "blue" + }, + "target_type": "ip", + "vpc_id": "\${data.aws_vpc.testPocketApp_pocket_vpc_C4E157E3.id}" + }, + "testPocketApp_ecs_service_green_target_group_ecs_target_group_0E777BA1": { + "deregistration_delay": "120", + "health_check": { + "healthy_threshold": 5, + "interval": 15, + "path": "/test", + "protocol": "HTTP", + "unhealthy_threshold": 3 + }, + "name_prefix": "TSTAPP", + "port": 80, + "protocol": "HTTP", + "tags": { + "type": "green" + }, + "target_type": "ip", + "vpc_id": "\${data.aws_vpc.testPocketApp_pocket_vpc_C4E157E3.id}" + } + }, + "aws_appautoscaling_policy": { + "testPocketApp_autoscaling_scale_in_policy_A163A235": { + "depends_on": [ + "aws_appautoscaling_target.testPocketApp_autoscaling_autoscaling_target_505E4EA1" + ], + "name": "testapp-ScaleInPolicy", + "policy_type": "StepScaling", + "resource_id": "\${aws_appautoscaling_target.testPocketApp_autoscaling_autoscaling_target_505E4EA1.resource_id}", + "scalable_dimension": "\${aws_appautoscaling_target.testPocketApp_autoscaling_autoscaling_target_505E4EA1.scalable_dimension}", + "service_namespace": "\${aws_appautoscaling_target.testPocketApp_autoscaling_autoscaling_target_505E4EA1.service_namespace}", + "step_scaling_policy_configuration": { + "adjustment_type": "ChangeInCapacity", + "cooldown": 60, + "metric_aggregation_type": "Average", + "step_adjustment": [ + { + "metric_interval_upper_bound": "0", + "scaling_adjustment": -1 + } + ] + }, + "target_tracking_scaling_policy_configuration": null + }, + "testPocketApp_autoscaling_scale_out_policy_075BFAC4": { + "depends_on": [ + "aws_appautoscaling_target.testPocketApp_autoscaling_autoscaling_target_505E4EA1" + ], + "name": "testapp-ScaleOutPolicy", + "policy_type": "StepScaling", + "resource_id": "\${aws_appautoscaling_target.testPocketApp_autoscaling_autoscaling_target_505E4EA1.resource_id}", + "scalable_dimension": "\${aws_appautoscaling_target.testPocketApp_autoscaling_autoscaling_target_505E4EA1.scalable_dimension}", + "service_namespace": "\${aws_appautoscaling_target.testPocketApp_autoscaling_autoscaling_target_505E4EA1.service_namespace}", + "step_scaling_policy_configuration": { + "adjustment_type": "ChangeInCapacity", + "cooldown": 60, + "metric_aggregation_type": "Average", + "step_adjustment": [ + { + "metric_interval_lower_bound": "0", + "scaling_adjustment": 2 + } + ] + }, + "target_tracking_scaling_policy_configuration": null + } + }, + "aws_appautoscaling_target": { + "testPocketApp_autoscaling_autoscaling_target_505E4EA1": { + "max_capacity": 2, + "min_capacity": 1, + "resource_id": "service/\${aws_ecs_cluster.testPocketApp_ecs_cluster_C3960066.name}/\${aws_ecs_service.testPocketApp_ecs_service_ecs-service_182DEA4C.name}", + "scalable_dimension": "ecs:service:DesiredCount", + "service_namespace": "ecs" + } + }, + "aws_cloudwatch_dashboard": { + "testPocketApp_cloudwatch-dashboard_26E9F70C": { + "dashboard_body": "{\\"widgets\\":[{\\"type\\":\\"metric\\",\\"x\\":0,\\"y\\":0,\\"width\\":12,\\"height\\":6,\\"properties\\":{\\"metrics\\":[[\\"AWS/ApplicationELB\\",\\"HTTPCode_Target_4XX_Count\\",\\"LoadBalancer\\",\\"\${aws_alb.testPocketApp_application_load_balancer_alb_D538DEEE.arn_suffix}\\",{\\"yAxis\\":\\"left\\",\\"color\\":\\"#ff7f0e\\"}],[\\".\\",\\"RequestCount\\",\\".\\",\\".\\",{\\"yAxis\\":\\"right\\",\\"color\\":\\"#1f77b4\\"}],[\\".\\",\\"HTTPCode_Target_5XX_Count\\",\\".\\",\\".\\",{\\"color\\":\\"#d62728\\"}],[\\".\\",\\"HTTPCode_Target_2XX_Count\\",\\".\\",\\".\\",{\\"yAxis\\":\\"right\\",\\"color\\":\\"#2ca02c\\"}]],\\"view\\":\\"timeSeries\\",\\"stacked\\":false,\\"region\\":\\"us-east-1\\",\\"period\\":60,\\"stat\\":\\"Sum\\",\\"title\\":\\"Target Requests\\"}},{\\"type\\":\\"metric\\",\\"x\\":12,\\"y\\":0,\\"width\\":12,\\"height\\":6,\\"properties\\":{\\"metrics\\":[[\\"AWS/ApplicationELB\\",\\"HTTPCode_ELB_4XX_Count\\",\\"LoadBalancer\\",\\"\${aws_alb.testPocketApp_application_load_balancer_alb_D538DEEE.arn_suffix}\\",{\\"yAxis\\":\\"left\\",\\"color\\":\\"#ff7f0e\\"}],[\\".\\",\\"RequestCount\\",\\".\\",\\".\\",{\\"yAxis\\":\\"right\\",\\"color\\":\\"#1f77b4\\"}],[\\".\\",\\"HTTPCode_ELB_5XX_Count\\",\\".\\",\\".\\",{\\"color\\":\\"#d62728\\"}]],\\"view\\":\\"timeSeries\\",\\"stacked\\":false,\\"region\\":\\"us-east-1\\",\\"period\\":60,\\"stat\\":\\"Sum\\",\\"title\\":\\"ALB Requests\\"}},{\\"type\\":\\"metric\\",\\"x\\":12,\\"y\\":6,\\"width\\":12,\\"height\\":6,\\"properties\\":{\\"metrics\\":[[\\"AWS/ApplicationELB\\",\\"TargetResponseTime\\",\\"LoadBalancer\\",\\"\${aws_alb.testPocketApp_application_load_balancer_alb_D538DEEE.arn_suffix}\\",{\\"label\\":\\"Average\\",\\"color\\":\\"#aec7e8\\"}],[\\"...\\",{\\"stat\\":\\"p95\\",\\"label\\":\\"p95\\",\\"color\\":\\"#ffbb78\\"}],[\\"...\\",{\\"stat\\":\\"p99\\",\\"label\\":\\"p99\\",\\"color\\":\\"#98df8a\\"}]],\\"view\\":\\"timeSeries\\",\\"stacked\\":false,\\"region\\":\\"us-east-1\\",\\"stat\\":\\"Average\\",\\"period\\":60}},{\\"type\\":\\"metric\\",\\"x\\":0,\\"y\\":6,\\"width\\":12,\\"height\\":6,\\"properties\\":{\\"metrics\\":[[\\"ECS/ContainerInsights\\",\\"RunningTaskCount\\",\\"ServiceName\\",\\"\${aws_ecs_service.testPocketApp_ecs_service_ecs-service_182DEA4C.name}\\",\\"ClusterName\\",\\"\${aws_ecs_cluster.testPocketApp_ecs_cluster_C3960066.name}\\",{\\"yAxis\\":\\"right\\",\\"color\\":\\"#c49c94\\"}],[\\"AWS/ECS\\",\\"CPUUtilization\\",\\".\\",\\".\\",\\".\\",\\".\\",{\\"color\\":\\"#f7b6d2\\"}],[\\".\\",\\"MemoryUtilization\\",\\".\\",\\".\\",\\".\\",\\".\\",{\\"color\\":\\"#c7c7c7\\"}]],\\"view\\":\\"timeSeries\\",\\"stacked\\":false,\\"region\\":\\"us-east-1\\",\\"stat\\":\\"Average\\",\\"period\\":60,\\"annotations\\":{\\"horizontal\\":[{\\"color\\":\\"#e377c2\\",\\"label\\":\\"CPU scale out\\",\\"value\\":45},{\\"color\\":\\"#c5b0d5\\",\\"label\\":\\"CPU scale in\\",\\"value\\":30}]},\\"title\\":\\"Service Load\\"}}]}", + "dashboard_name": "testapp", + "lifecycle": { + "ignore_changes": [ + "dashboard_body" + ] + } + } + }, + "aws_cloudwatch_metric_alarm": { + "testPocketApp_autoscaling_scale_in_alarm_69B5F00D": { + "alarm_actions": [ + "\${aws_appautoscaling_policy.testPocketApp_autoscaling_scale_in_policy_A163A235.arn}" + ], + "alarm_description": "Alarm to reduce capacity if container CPU is low", + "alarm_name": "testapp Service Low CPU", + "comparison_operator": "LessThanThreshold", + "dimensions": { + "ClusterName": "\${aws_ecs_cluster.testPocketApp_ecs_cluster_C3960066.name}", + "ServiceName": "\${aws_ecs_service.testPocketApp_ecs_service_ecs-service_182DEA4C.name}" + }, + "evaluation_periods": 2, + "metric_name": "CPUUtilization", + "namespace": "AWS/ECS", + "period": 60, + "statistic": "Average", + "threshold": 30, + "treat_missing_data": "notBreaching" + }, + "testPocketApp_autoscaling_scale_out_alarm_4313FBE9": { + "alarm_actions": [ + "\${aws_appautoscaling_policy.testPocketApp_autoscaling_scale_out_policy_075BFAC4.arn}" + ], + "alarm_description": "Alarm to add capacity if container CPU is high", + "alarm_name": "testapp Service High CPU", + "comparison_operator": "GreaterThanThreshold", + "dimensions": { + "ClusterName": "\${aws_ecs_cluster.testPocketApp_ecs_cluster_C3960066.name}", + "ServiceName": "\${aws_ecs_service.testPocketApp_ecs_service_ecs-service_182DEA4C.name}" + }, + "evaluation_periods": 2, + "metric_name": "CPUUtilization", + "namespace": "AWS/ECS", + "period": 60, + "statistic": "Average", + "threshold": 45, + "treat_missing_data": "notBreaching" + } + }, + "aws_codedeploy_app": { + "testPocketApp_ecs_service_ecs_codedeploy_ecs_code_deploy_480D0565": { + "compute_platform": "ECS", + "name": "testapp-ECS" + } + }, + "aws_codedeploy_deployment_group": { + "testPocketApp_ecs_service_ecs_codedeploy_ecs_codedeploy_deployment_group_44B006D1": { + "app_name": "\${aws_codedeploy_app.testPocketApp_ecs_service_ecs_codedeploy_ecs_code_deploy_480D0565.name}", + "auto_rollback_configuration": { + "enabled": true, + "events": [ + "DEPLOYMENT_FAILURE" + ] + }, + "blue_green_deployment_config": { + "deployment_ready_option": { + "action_on_timeout": "CONTINUE_DEPLOYMENT" + }, + "terminate_blue_instances_on_deployment_success": { + "action": "TERMINATE", + "termination_wait_time_in_minutes": 5 + } + }, + "depends_on": [ + "aws_ecs_service.testPocketApp_ecs_service_ecs-service_182DEA4C" + ], + "deployment_config_name": "CodeDeployDefault.ECSAllAtOnce", + "deployment_group_name": "testapp-ECS", + "deployment_style": { + "deployment_option": "WITH_TRAFFIC_CONTROL", + "deployment_type": "BLUE_GREEN" + }, + "ecs_service": { + "cluster_name": "\${aws_ecs_cluster.testPocketApp_ecs_cluster_C3960066.name}", + "service_name": "\${aws_ecs_service.testPocketApp_ecs_service_ecs-service_182DEA4C.name}" + }, + "load_balancer_info": { + "target_group_pair_info": { + "prod_traffic_route": { + "listener_arns": [ + "\${aws_alb_listener.testPocketApp_listener_https_7F49DE83.arn}" + ] + }, + "target_group": [ + { + "name": "\${aws_alb_target_group.testPocketApp_ecs_service_blue_target_group_ecs_target_group_A4B85885.name}" + }, + { + "name": "\${aws_alb_target_group.testPocketApp_ecs_service_green_target_group_ecs_target_group_0E777BA1.name}" + } + ] + } + }, + "service_role_arn": "\${aws_iam_role.testPocketApp_ecs_service_ecs_codedeploy_ecs_code_deploy_role_287C837D.arn}" + } + }, + "aws_ecs_cluster": { + "testPocketApp_ecs_cluster_C3960066": { + "name": "testapp", + "setting": [ + { + "name": "containerInsights", + "value": "enabled" + } + ] + } + }, + "aws_ecs_service": { + "testPocketApp_ecs_service_ecs-service_182DEA4C": { + "cluster": "\${aws_ecs_cluster.testPocketApp_ecs_cluster_C3960066.arn}", + "depends_on": [ + "aws_alb_target_group.testPocketApp_ecs_service_blue_target_group_ecs_target_group_A4B85885", + "aws_alb_listener_rule.testPocketApp_ecs_service_listener_rule_C03BB04D" + ], + "deployment_controller": { + "type": "CODE_DEPLOY" + }, + "deployment_maximum_percent": 200, + "deployment_minimum_healthy_percent": 100, + "desired_count": 2, + "launch_type": "FARGATE", + "lifecycle": { + "ignore_changes": [ + "desired_count", + "load_balancer", + "task_definition" + ] + }, + "load_balancer": [ + { + "container_name": "main_container", + "container_port": 8675309, + "target_group_arn": "\${aws_alb_target_group.testPocketApp_ecs_service_blue_target_group_ecs_target_group_A4B85885.arn}" + } + ], + "name": "testapp", + "network_configuration": { + "security_groups": [ + "\${aws_security_group.testPocketApp_ecs_service_ecs_security_group_9C016FA8.id}" + ], + "subnets": "\${data.aws_subnets.testPocketApp_pocket_vpc_private_subnet_ids_EB1E3A65.ids}" + }, + "propagate_tags": "SERVICE", + "task_definition": "\${aws_ecs_task_definition.testPocketApp_ecs_service_ecs-task_A7E74E45.arn}" + } + }, + "aws_ecs_task_definition": { + "testPocketApp_ecs_service_ecs-task_A7E74E45": { + "container_definitions": "[]", + "cpu": "512", + "depends_on": [ + ], + "execution_role_arn": "\${aws_iam_role.testPocketApp_ecs_service_ecs-iam_ecs-execution-role_EB461A0C.arn}", + "family": "testapp", + "memory": "2048", + "network_mode": "awsvpc", + "requires_compatibilities": [ + "FARGATE" + ], + "task_role_arn": "\${aws_iam_role.testPocketApp_ecs_service_ecs-iam_ecs-task-role_7DB93963.arn}", + "volume": [ + ] + } + }, + "aws_iam_role": { + "testPocketApp_ecs_service_ecs-iam_ecs-execution-role_EB461A0C": { + "assume_role_policy": "\${data.aws_iam_policy_document.testPocketApp_ecs_service_ecs-iam_ecs-task-assume_ABB81680.json}", + "name": "testapp-TaskExecutionRole" + }, + "testPocketApp_ecs_service_ecs-iam_ecs-task-role_7DB93963": { + "assume_role_policy": "\${data.aws_iam_policy_document.testPocketApp_ecs_service_ecs-iam_ecs-task-assume_ABB81680.json}", + "name": "testapp-TaskRole" + }, + "testPocketApp_ecs_service_ecs_codedeploy_ecs_code_deploy_role_287C837D": { + "assume_role_policy": "\${data.aws_iam_policy_document.testPocketApp_ecs_service_ecs_codedeploy_codedeploy_assume_role_840D1ECF.json}", + "name": "testapp-ECSCodeDeployRole" + } + }, + "aws_iam_role_policy_attachment": { + "testPocketApp_ecs_service_ecs_codedeploy_ecs_codedeploy_role_attachment_F9A8369F": { + "depends_on": [ + "aws_iam_role.testPocketApp_ecs_service_ecs_codedeploy_ecs_code_deploy_role_287C837D" + ], + "policy_arn": "arn:aws:iam::aws:policy/AWSCodeDeployRoleForECS", + "role": "\${aws_iam_role.testPocketApp_ecs_service_ecs_codedeploy_ecs_code_deploy_role_287C837D.name}" + } + }, + "aws_route53_record": { + "testPocketApp_alb_certificate_certificate_record_3C49201F": { + "depends_on": [ + "aws_acm_certificate.testPocketApp_alb_certificate_417C14FF" + ], + "name": "\${tolist(aws_acm_certificate.testPocketApp_alb_certificate_417C14FF.domain_validation_options).0.resource_record_name}", + "records": [ + "\${tolist(aws_acm_certificate.testPocketApp_alb_certificate_417C14FF.domain_validation_options).0.resource_record_value}" + ], + "ttl": 60, + "type": "\${tolist(aws_acm_certificate.testPocketApp_alb_certificate_417C14FF.domain_validation_options).0.resource_record_type}", + "zone_id": "\${aws_route53_zone.testPocketApp_base_dns_subhosted_zone_1E1E3243.zone_id}" + }, + "testPocketApp_alb_record_7B56ED33": { + "alias": { + "evaluate_target_health": true, + "name": "\${aws_alb.testPocketApp_application_load_balancer_alb_D538DEEE.dns_name}", + "zone_id": "\${aws_alb.testPocketApp_application_load_balancer_alb_D538DEEE.zone_id}" + }, + "lifecycle": { + "ignore_changes": [ + "weighted_routing_policy[0].weight" + ] + }, + "name": "testing.bowling.gov", + "set_identifier": "1", + "type": "A", + "weighted_routing_policy": { + "weight": 1 + }, + "zone_id": "\${aws_route53_zone.testPocketApp_base_dns_subhosted_zone_1E1E3243.zone_id}" + }, + "testPocketApp_base_dns_subhosted_zone_ns_497FFB83": { + "name": "testing.bowling.gov", + "records": "\${aws_route53_zone.testPocketApp_base_dns_subhosted_zone_1E1E3243.name_servers}", + "ttl": 86400, + "type": "NS", + "zone_id": "\${data.aws_route53_zone.testPocketApp_base_dns_main_hosted_zone_9442EEB0.zone_id}" + } + }, + "aws_route53_zone": { + "testPocketApp_base_dns_subhosted_zone_1E1E3243": { + "name": "testing.bowling.gov" + } + }, + "aws_security_group": { + "testPocketApp_application_load_balancer_alb_security_group_91A27046": { + "description": "External security group (Managed by Terraform)", + "egress": [ + { + "cidr_blocks": [ + "0.0.0.0/0" + ], + "description": "required", + "from_port": 0, + "ipv6_cidr_blocks": [ + ], + "prefix_list_ids": [ + ], + "protocol": "-1", + "security_groups": [ + ], + "self": null, + "to_port": 0 + } + ], + "ingress": [ + { + "cidr_blocks": [ + "0.0.0.0/0" + ], + "description": null, + "from_port": 443, + "ipv6_cidr_blocks": null, + "prefix_list_ids": null, + "protocol": "TCP", + "security_groups": null, + "self": null, + "to_port": 443 + }, + { + "cidr_blocks": [ + "0.0.0.0/0" + ], + "description": null, + "from_port": 80, + "ipv6_cidr_blocks": null, + "prefix_list_ids": null, + "protocol": "TCP", + "security_groups": null, + "self": null, + "to_port": 80 + } + ], + "lifecycle": { + "create_before_destroy": true + }, + "name_prefix": "testapp-HTTP/S Security Group", + "tags": { + "Name": "testapp-HTTP/S Security Group" + }, + "vpc_id": "\${data.aws_vpc.testPocketApp_pocket_vpc_C4E157E3.id}" + }, + "testPocketApp_ecs_service_ecs_security_group_9C016FA8": { + "description": "Internal ECS Security Group (Managed by Terraform)", + "egress": [ + { + "cidr_blocks": [ + "0.0.0.0/0" + ], + "description": "required", + "from_port": 0, + "ipv6_cidr_blocks": [ + ], + "prefix_list_ids": [ + ], + "protocol": "-1", + "security_groups": [ + ], + "self": null, + "to_port": 0 + } + ], + "ingress": [ + { + "cidr_blocks": [ + ], + "description": "required", + "from_port": 80, + "ipv6_cidr_blocks": [ + ], + "prefix_list_ids": [ + ], + "protocol": "TCP", + "security_groups": [ + "\${aws_security_group.testPocketApp_application_load_balancer_alb_security_group_91A27046.id}" + ], + "self": null, + "to_port": 8675309 + } + ], + "lifecycle": { + "create_before_destroy": true + }, + "name_prefix": "testapp-ECSSecurityGroup", + "vpc_id": "\${data.aws_vpc.testPocketApp_pocket_vpc_C4E157E3.id}" + } + }, + "local_file": { + "testPocketApp_ecs_service_appspec_6EB2FE4D": { + "content": "{\\"version\\":1,\\"Resources\\":[{\\"TargetService\\":{\\"Type\\":\\"AWS::ECS::Service\\",\\"Properties\\":{\\"TaskDefinition\\":\\"\${aws_ecs_task_definition.testPocketApp_ecs_service_ecs-task_A7E74E45.arn}\\",\\"LoadBalancerInfo\\":{\\"ContainerName\\":\\"main_container\\",\\"ContainerPort\\":8675309}}}}]}", + "filename": "appspec.json" + } + }, + "null_resource": { + "testPocketApp_ecs_service_update-task-definition_23CCF5D1": { + "depends_on": [ + "aws_ecs_task_definition.testPocketApp_ecs_service_ecs-task_A7E74E45", + "aws_codedeploy_app.testPocketApp_ecs_service_ecs_codedeploy_ecs_code_deploy_480D0565", + "aws_codedeploy_deployment_group.testPocketApp_ecs_service_ecs_codedeploy_ecs_codedeploy_deployment_group_44B006D1" + ], + "provisioner": { + "local-exec": { + "command": "export app_spec_content_string='{\\"version\\":1,\\"Resources\\":[{\\"TargetService\\":{\\"Type\\":\\"AWS::ECS::Service\\",\\"Properties\\":{\\"TaskDefinition\\":\\"\${aws_ecs_task_definition.testPocketApp_ecs_service_ecs-task_A7E74E45.arn}\\",\\"LoadBalancerInfo\\":{\\"ContainerName\\":\\"main_container\\",\\"ContainerPort\\":8675309}}}}]}' && export revision=\\"revisionType=AppSpecContent,appSpecContent={content='$app_spec_content_string'}\\" && aws --region us-east-1 deploy create-deployment --application-name=\\"\${aws_codedeploy_app.testPocketApp_ecs_service_ecs_codedeploy_ecs_code_deploy_480D0565.name}\\" --deployment-group-name=\\"\${aws_codedeploy_deployment_group.testPocketApp_ecs_service_ecs_codedeploy_ecs_codedeploy_deployment_group_44B006D1.deployment_group_name}\\" --description=\\"Triggered from Terraform/CodeBuild due to a task definition update\\" --revision=\\"$revision\\"" + } + }, + "triggers": { + "task_arn": "\${aws_ecs_task_definition.testPocketApp_ecs_service_ecs-task_A7E74E45.arn}" + } + } + } + } +}" +`; + +exports[`PocketALBApplication renders an Pocket App with code deploy and creates appspec.json and taskdef.json when using code pipeline 1`] = ` +"{ + "data": { + "aws_caller_identity": { + "testPocketApp_pocket_vpc_current_identity_06D87057": { + } + }, + "aws_iam_policy_document": { + "testPocketApp_ecs_service_ecs-iam_ecs-task-assume_ABB81680": { + "statement": [ + { + "actions": [ + "sts:AssumeRole" + ], + "effect": "Allow", + "principals": [ + { + "identifiers": [ + "ecs-tasks.amazonaws.com" + ], + "type": "Service" + } + ] + } + ], + "version": "2012-10-17" + }, + "testPocketApp_ecs_service_ecs_codedeploy_codedeploy_assume_role_840D1ECF": { + "statement": [ + { + "actions": [ + "sts:AssumeRole" + ], + "effect": "Allow", + "principals": [ + { + "identifiers": [ + "codedeploy.amazonaws.com" + ], + "type": "Service" + } + ] + } + ] + } + }, + "aws_kms_alias": { + "testPocketApp_pocket_vpc_secrets_manager_key_9FD2BC93": { + "name": "alias/aws/secretsmanager" + } + }, + "aws_region": { + "testPocketApp_pocket_vpc_current_region_8ED435E7": { + } + }, + "aws_route53_zone": { + "testPocketApp_base_dns_main_hosted_zone_9442EEB0": { + "name": "bowling.gov" + } + }, + "aws_security_groups": { + "testPocketApp_pocket_vpc_default_security_groups_40B4FC48": { + "filter": [ + { + "name": "group-name", + "values": [ + "default" + ] + }, + { + "name": "vpc-id", + "values": [ + "\${data.aws_vpc.testPocketApp_pocket_vpc_C4E157E3.id}" + ] + } + ] + }, + "testPocketApp_pocket_vpc_internal_security_groups_12F7F666": { + "filter": [ + { + "name": "group-name", + "values": [ + "pocket-vpc-internal" + ] + }, + { + "name": "vpc-id", + "values": [ + "\${data.aws_vpc.testPocketApp_pocket_vpc_C4E157E3.id}" + ] + } + ] + } + }, + "aws_ssm_parameter": { + "testPocketApp_pocket_vpc_private_subnets_9D449563": { + "name": "/Shared/PrivateSubnets" + }, + "testPocketApp_pocket_vpc_public_subnets_B0B3A6AB": { + "name": "/Shared/PublicSubnets" + }, + "testPocketApp_pocket_vpc_vpc_ssm_param_235525D4": { + "name": "/Shared/Vpc" + } + }, + "aws_subnets": { + "testPocketApp_pocket_vpc_private_subnet_ids_EB1E3A65": { + "filter": [ + { + "name": "subnet-id", + "values": "\${split(\\",\\", data.aws_ssm_parameter.testPocketApp_pocket_vpc_private_subnets_9D449563.value)}" + }, + { + "name": "vpc-id", + "values": [ + "\${data.aws_vpc.testPocketApp_pocket_vpc_C4E157E3.id}" + ] + } + ] + }, + "testPocketApp_pocket_vpc_public_subnet_ids_01F0B902": { + "filter": [ + { + "name": "subnet-id", + "values": "\${split(\\",\\", data.aws_ssm_parameter.testPocketApp_pocket_vpc_public_subnets_B0B3A6AB.value)}" + }, + { + "name": "vpc-id", + "values": [ + "\${data.aws_vpc.testPocketApp_pocket_vpc_C4E157E3.id}" + ] + } + ] + } + }, + "aws_vpc": { + "testPocketApp_pocket_vpc_C4E157E3": { + "filter": [ + { + "name": "vpc-id", + "values": [ + "\${data.aws_ssm_parameter.testPocketApp_pocket_vpc_vpc_ssm_param_235525D4.value}" + ] + } + ] + } + } + }, + "resource": { + "aws_acm_certificate": { + "testPocketApp_alb_certificate_417C14FF": { + "domain_name": "testing.bowling.gov", + "lifecycle": { + "create_before_destroy": true + }, + "validation_method": "DNS" + } + }, + "aws_acm_certificate_validation": { + "testPocketApp_alb_certificate_certificate_validation_7D899C6A": { + "certificate_arn": "\${aws_acm_certificate.testPocketApp_alb_certificate_417C14FF.arn}", + "depends_on": [ + "aws_route53_record.testPocketApp_alb_certificate_certificate_record_3C49201F", + "aws_acm_certificate.testPocketApp_alb_certificate_417C14FF" + ], + "validation_record_fqdns": [ + "\${aws_route53_record.testPocketApp_alb_certificate_certificate_record_3C49201F.fqdn}" + ] + } + }, + "aws_alb": { + "testPocketApp_application_load_balancer_alb_D538DEEE": { + "internal": false, + "name_prefix": "TSTAPP", + "security_groups": [ + "\${aws_security_group.testPocketApp_application_load_balancer_alb_security_group_91A27046.id}" + ], + "subnets": "\${data.aws_subnets.testPocketApp_pocket_vpc_public_subnet_ids_01F0B902.ids}" + } + }, + "aws_alb_listener": { + "testPocketApp_listener_http_A9E227C2": { + "default_action": [ + { + "redirect": { + "port": "443", + "protocol": "HTTPS", + "status_code": "HTTP_301" + }, + "type": "redirect" + } + ], + "load_balancer_arn": "\${aws_alb.testPocketApp_application_load_balancer_alb_D538DEEE.arn}", + "port": 80, + "protocol": "HTTP" + }, + "testPocketApp_listener_https_7F49DE83": { + "certificate_arn": "\${aws_acm_certificate.testPocketApp_alb_certificate_417C14FF.arn}", + "default_action": [ + { + "fixed_response": { + "content_type": "text/plain", + "message_body": "", + "status_code": "503" + }, + "type": "fixed-response" + } + ], + "load_balancer_arn": "\${aws_alb.testPocketApp_application_load_balancer_alb_D538DEEE.arn}", + "port": 443, + "protocol": "HTTPS", + "ssl_policy": "ELBSecurityPolicy-TLS-1-1-2017-01" + } + }, + "aws_alb_listener_rule": { + "testPocketApp_ecs_service_listener_rule_C03BB04D": { + "action": [ + { + "target_group_arn": "\${aws_alb_target_group.testPocketApp_ecs_service_blue_target_group_ecs_target_group_A4B85885.arn}", + "type": "forward" + } + ], + "condition": [ + { + "path_pattern": { + "values": [ + "*" + ] + } + } + ], + "lifecycle": { + "ignore_changes": [ + "action" + ] + }, + "listener_arn": "\${aws_alb_listener.testPocketApp_listener_https_7F49DE83.arn}", + "priority": 1 + } + }, + "aws_alb_target_group": { + "testPocketApp_ecs_service_blue_target_group_ecs_target_group_A4B85885": { + "deregistration_delay": "120", + "health_check": { + "healthy_threshold": 5, + "interval": 15, + "path": "/test", + "protocol": "HTTP", + "unhealthy_threshold": 3 + }, + "name_prefix": "TSTAPP", + "port": 80, + "protocol": "HTTP", + "tags": { + "type": "blue" + }, + "target_type": "ip", + "vpc_id": "\${data.aws_vpc.testPocketApp_pocket_vpc_C4E157E3.id}" + }, + "testPocketApp_ecs_service_green_target_group_ecs_target_group_0E777BA1": { + "deregistration_delay": "120", + "health_check": { + "healthy_threshold": 5, + "interval": 15, + "path": "/test", + "protocol": "HTTP", + "unhealthy_threshold": 3 + }, + "name_prefix": "TSTAPP", + "port": 80, + "protocol": "HTTP", + "tags": { + "type": "green" + }, + "target_type": "ip", + "vpc_id": "\${data.aws_vpc.testPocketApp_pocket_vpc_C4E157E3.id}" + } + }, + "aws_appautoscaling_policy": { + "testPocketApp_autoscaling_scale_in_policy_A163A235": { + "depends_on": [ + "aws_appautoscaling_target.testPocketApp_autoscaling_autoscaling_target_505E4EA1" + ], + "name": "testapp-ScaleInPolicy", + "policy_type": "StepScaling", + "resource_id": "\${aws_appautoscaling_target.testPocketApp_autoscaling_autoscaling_target_505E4EA1.resource_id}", + "scalable_dimension": "\${aws_appautoscaling_target.testPocketApp_autoscaling_autoscaling_target_505E4EA1.scalable_dimension}", + "service_namespace": "\${aws_appautoscaling_target.testPocketApp_autoscaling_autoscaling_target_505E4EA1.service_namespace}", + "step_scaling_policy_configuration": { + "adjustment_type": "ChangeInCapacity", + "cooldown": 60, + "metric_aggregation_type": "Average", + "step_adjustment": [ + { + "metric_interval_upper_bound": "0", + "scaling_adjustment": -1 + } + ] + }, + "target_tracking_scaling_policy_configuration": null + }, + "testPocketApp_autoscaling_scale_out_policy_075BFAC4": { + "depends_on": [ + "aws_appautoscaling_target.testPocketApp_autoscaling_autoscaling_target_505E4EA1" + ], + "name": "testapp-ScaleOutPolicy", + "policy_type": "StepScaling", + "resource_id": "\${aws_appautoscaling_target.testPocketApp_autoscaling_autoscaling_target_505E4EA1.resource_id}", + "scalable_dimension": "\${aws_appautoscaling_target.testPocketApp_autoscaling_autoscaling_target_505E4EA1.scalable_dimension}", + "service_namespace": "\${aws_appautoscaling_target.testPocketApp_autoscaling_autoscaling_target_505E4EA1.service_namespace}", + "step_scaling_policy_configuration": { + "adjustment_type": "ChangeInCapacity", + "cooldown": 60, + "metric_aggregation_type": "Average", + "step_adjustment": [ + { + "metric_interval_lower_bound": "0", + "scaling_adjustment": 2 + } + ] + }, + "target_tracking_scaling_policy_configuration": null + } + }, + "aws_appautoscaling_target": { + "testPocketApp_autoscaling_autoscaling_target_505E4EA1": { + "max_capacity": 2, + "min_capacity": 1, + "resource_id": "service/\${aws_ecs_cluster.testPocketApp_ecs_cluster_C3960066.name}/\${aws_ecs_service.testPocketApp_ecs_service_ecs-service_182DEA4C.name}", + "scalable_dimension": "ecs:service:DesiredCount", + "service_namespace": "ecs" + } + }, + "aws_cloudwatch_dashboard": { + "testPocketApp_cloudwatch-dashboard_26E9F70C": { + "dashboard_body": "{\\"widgets\\":[{\\"type\\":\\"metric\\",\\"x\\":0,\\"y\\":0,\\"width\\":12,\\"height\\":6,\\"properties\\":{\\"metrics\\":[[\\"AWS/ApplicationELB\\",\\"HTTPCode_Target_4XX_Count\\",\\"LoadBalancer\\",\\"\${aws_alb.testPocketApp_application_load_balancer_alb_D538DEEE.arn_suffix}\\",{\\"yAxis\\":\\"left\\",\\"color\\":\\"#ff7f0e\\"}],[\\".\\",\\"RequestCount\\",\\".\\",\\".\\",{\\"yAxis\\":\\"right\\",\\"color\\":\\"#1f77b4\\"}],[\\".\\",\\"HTTPCode_Target_5XX_Count\\",\\".\\",\\".\\",{\\"color\\":\\"#d62728\\"}],[\\".\\",\\"HTTPCode_Target_2XX_Count\\",\\".\\",\\".\\",{\\"yAxis\\":\\"right\\",\\"color\\":\\"#2ca02c\\"}]],\\"view\\":\\"timeSeries\\",\\"stacked\\":false,\\"region\\":\\"us-east-1\\",\\"period\\":60,\\"stat\\":\\"Sum\\",\\"title\\":\\"Target Requests\\"}},{\\"type\\":\\"metric\\",\\"x\\":12,\\"y\\":0,\\"width\\":12,\\"height\\":6,\\"properties\\":{\\"metrics\\":[[\\"AWS/ApplicationELB\\",\\"HTTPCode_ELB_4XX_Count\\",\\"LoadBalancer\\",\\"\${aws_alb.testPocketApp_application_load_balancer_alb_D538DEEE.arn_suffix}\\",{\\"yAxis\\":\\"left\\",\\"color\\":\\"#ff7f0e\\"}],[\\".\\",\\"RequestCount\\",\\".\\",\\".\\",{\\"yAxis\\":\\"right\\",\\"color\\":\\"#1f77b4\\"}],[\\".\\",\\"HTTPCode_ELB_5XX_Count\\",\\".\\",\\".\\",{\\"color\\":\\"#d62728\\"}]],\\"view\\":\\"timeSeries\\",\\"stacked\\":false,\\"region\\":\\"us-east-1\\",\\"period\\":60,\\"stat\\":\\"Sum\\",\\"title\\":\\"ALB Requests\\"}},{\\"type\\":\\"metric\\",\\"x\\":12,\\"y\\":6,\\"width\\":12,\\"height\\":6,\\"properties\\":{\\"metrics\\":[[\\"AWS/ApplicationELB\\",\\"TargetResponseTime\\",\\"LoadBalancer\\",\\"\${aws_alb.testPocketApp_application_load_balancer_alb_D538DEEE.arn_suffix}\\",{\\"label\\":\\"Average\\",\\"color\\":\\"#aec7e8\\"}],[\\"...\\",{\\"stat\\":\\"p95\\",\\"label\\":\\"p95\\",\\"color\\":\\"#ffbb78\\"}],[\\"...\\",{\\"stat\\":\\"p99\\",\\"label\\":\\"p99\\",\\"color\\":\\"#98df8a\\"}]],\\"view\\":\\"timeSeries\\",\\"stacked\\":false,\\"region\\":\\"us-east-1\\",\\"stat\\":\\"Average\\",\\"period\\":60}},{\\"type\\":\\"metric\\",\\"x\\":0,\\"y\\":6,\\"width\\":12,\\"height\\":6,\\"properties\\":{\\"metrics\\":[[\\"ECS/ContainerInsights\\",\\"RunningTaskCount\\",\\"ServiceName\\",\\"\${aws_ecs_service.testPocketApp_ecs_service_ecs-service_182DEA4C.name}\\",\\"ClusterName\\",\\"\${aws_ecs_cluster.testPocketApp_ecs_cluster_C3960066.name}\\",{\\"yAxis\\":\\"right\\",\\"color\\":\\"#c49c94\\"}],[\\"AWS/ECS\\",\\"CPUUtilization\\",\\".\\",\\".\\",\\".\\",\\".\\",{\\"color\\":\\"#f7b6d2\\"}],[\\".\\",\\"MemoryUtilization\\",\\".\\",\\".\\",\\".\\",\\".\\",{\\"color\\":\\"#c7c7c7\\"}]],\\"view\\":\\"timeSeries\\",\\"stacked\\":false,\\"region\\":\\"us-east-1\\",\\"stat\\":\\"Average\\",\\"period\\":60,\\"annotations\\":{\\"horizontal\\":[{\\"color\\":\\"#e377c2\\",\\"label\\":\\"CPU scale out\\",\\"value\\":45},{\\"color\\":\\"#c5b0d5\\",\\"label\\":\\"CPU scale in\\",\\"value\\":30}]},\\"title\\":\\"Service Load\\"}}]}", + "dashboard_name": "testapp", + "lifecycle": { + "ignore_changes": [ + "dashboard_body" + ] + } + } + }, + "aws_cloudwatch_metric_alarm": { + "testPocketApp_autoscaling_scale_in_alarm_69B5F00D": { + "alarm_actions": [ + "\${aws_appautoscaling_policy.testPocketApp_autoscaling_scale_in_policy_A163A235.arn}" + ], + "alarm_description": "Alarm to reduce capacity if container CPU is low", + "alarm_name": "testapp Service Low CPU", + "comparison_operator": "LessThanThreshold", + "dimensions": { + "ClusterName": "\${aws_ecs_cluster.testPocketApp_ecs_cluster_C3960066.name}", + "ServiceName": "\${aws_ecs_service.testPocketApp_ecs_service_ecs-service_182DEA4C.name}" + }, + "evaluation_periods": 2, + "metric_name": "CPUUtilization", + "namespace": "AWS/ECS", + "period": 60, + "statistic": "Average", + "threshold": 30, + "treat_missing_data": "notBreaching" + }, + "testPocketApp_autoscaling_scale_out_alarm_4313FBE9": { + "alarm_actions": [ + "\${aws_appautoscaling_policy.testPocketApp_autoscaling_scale_out_policy_075BFAC4.arn}" + ], + "alarm_description": "Alarm to add capacity if container CPU is high", + "alarm_name": "testapp Service High CPU", + "comparison_operator": "GreaterThanThreshold", + "dimensions": { + "ClusterName": "\${aws_ecs_cluster.testPocketApp_ecs_cluster_C3960066.name}", + "ServiceName": "\${aws_ecs_service.testPocketApp_ecs_service_ecs-service_182DEA4C.name}" + }, + "evaluation_periods": 2, + "metric_name": "CPUUtilization", + "namespace": "AWS/ECS", + "period": 60, + "statistic": "Average", + "threshold": 45, + "treat_missing_data": "notBreaching" + } + }, + "aws_codedeploy_app": { + "testPocketApp_ecs_service_ecs_codedeploy_ecs_code_deploy_480D0565": { + "compute_platform": "ECS", + "name": "testapp-ECS" + } + }, + "aws_codedeploy_deployment_group": { + "testPocketApp_ecs_service_ecs_codedeploy_ecs_codedeploy_deployment_group_44B006D1": { + "app_name": "\${aws_codedeploy_app.testPocketApp_ecs_service_ecs_codedeploy_ecs_code_deploy_480D0565.name}", + "auto_rollback_configuration": { + "enabled": true, + "events": [ + "DEPLOYMENT_FAILURE" + ] + }, + "blue_green_deployment_config": { + "deployment_ready_option": { + "action_on_timeout": "CONTINUE_DEPLOYMENT" + }, + "terminate_blue_instances_on_deployment_success": { + "action": "TERMINATE", + "termination_wait_time_in_minutes": 5 + } + }, + "depends_on": [ + "aws_ecs_service.testPocketApp_ecs_service_ecs-service_182DEA4C" + ], + "deployment_config_name": "CodeDeployDefault.ECSAllAtOnce", + "deployment_group_name": "testapp-ECS", + "deployment_style": { + "deployment_option": "WITH_TRAFFIC_CONTROL", + "deployment_type": "BLUE_GREEN" + }, + "ecs_service": { + "cluster_name": "\${aws_ecs_cluster.testPocketApp_ecs_cluster_C3960066.name}", + "service_name": "\${aws_ecs_service.testPocketApp_ecs_service_ecs-service_182DEA4C.name}" + }, + "load_balancer_info": { + "target_group_pair_info": { + "prod_traffic_route": { + "listener_arns": [ + "\${aws_alb_listener.testPocketApp_listener_https_7F49DE83.arn}" + ] + }, + "target_group": [ + { + "name": "\${aws_alb_target_group.testPocketApp_ecs_service_blue_target_group_ecs_target_group_A4B85885.name}" + }, + { + "name": "\${aws_alb_target_group.testPocketApp_ecs_service_green_target_group_ecs_target_group_0E777BA1.name}" + } + ] + } + }, + "service_role_arn": "\${aws_iam_role.testPocketApp_ecs_service_ecs_codedeploy_ecs_code_deploy_role_287C837D.arn}" + } + }, + "aws_ecs_cluster": { + "testPocketApp_ecs_cluster_C3960066": { + "name": "testapp", + "setting": [ + { + "name": "containerInsights", + "value": "enabled" + } + ] + } + }, + "aws_ecs_service": { + "testPocketApp_ecs_service_ecs-service_182DEA4C": { + "cluster": "\${aws_ecs_cluster.testPocketApp_ecs_cluster_C3960066.arn}", + "depends_on": [ + "aws_alb_target_group.testPocketApp_ecs_service_blue_target_group_ecs_target_group_A4B85885", + "aws_alb_listener_rule.testPocketApp_ecs_service_listener_rule_C03BB04D" + ], + "deployment_controller": { + "type": "CODE_DEPLOY" + }, + "deployment_maximum_percent": 200, + "deployment_minimum_healthy_percent": 100, + "desired_count": 2, + "launch_type": "FARGATE", + "lifecycle": { + "ignore_changes": [ + "desired_count", + "load_balancer", + "task_definition" + ] + }, + "load_balancer": [ + { + "container_name": "main_container", + "container_port": 8675309, + "target_group_arn": "\${aws_alb_target_group.testPocketApp_ecs_service_blue_target_group_ecs_target_group_A4B85885.arn}" + } + ], + "name": "testapp", + "network_configuration": { + "security_groups": [ + "\${aws_security_group.testPocketApp_ecs_service_ecs_security_group_9C016FA8.id}" + ], + "subnets": "\${data.aws_subnets.testPocketApp_pocket_vpc_private_subnet_ids_EB1E3A65.ids}" + }, + "propagate_tags": "SERVICE", + "task_definition": "\${aws_ecs_task_definition.testPocketApp_ecs_service_ecs-task_A7E74E45.arn}" + } + }, + "aws_ecs_task_definition": { + "testPocketApp_ecs_service_ecs-task_A7E74E45": { + "container_definitions": "[]", + "cpu": "512", + "depends_on": [ + ], + "execution_role_arn": "\${aws_iam_role.testPocketApp_ecs_service_ecs-iam_ecs-execution-role_EB461A0C.arn}", + "family": "testapp", + "memory": "2048", + "network_mode": "awsvpc", + "requires_compatibilities": [ + "FARGATE" + ], + "task_role_arn": "\${aws_iam_role.testPocketApp_ecs_service_ecs-iam_ecs-task-role_7DB93963.arn}", + "volume": [ + ] + } + }, + "aws_iam_role": { + "testPocketApp_ecs_service_ecs-iam_ecs-execution-role_EB461A0C": { + "assume_role_policy": "\${data.aws_iam_policy_document.testPocketApp_ecs_service_ecs-iam_ecs-task-assume_ABB81680.json}", + "name": "testapp-TaskExecutionRole" + }, + "testPocketApp_ecs_service_ecs-iam_ecs-task-role_7DB93963": { + "assume_role_policy": "\${data.aws_iam_policy_document.testPocketApp_ecs_service_ecs-iam_ecs-task-assume_ABB81680.json}", + "name": "testapp-TaskRole" + }, + "testPocketApp_ecs_service_ecs_codedeploy_ecs_code_deploy_role_287C837D": { + "assume_role_policy": "\${data.aws_iam_policy_document.testPocketApp_ecs_service_ecs_codedeploy_codedeploy_assume_role_840D1ECF.json}", + "name": "testapp-ECSCodeDeployRole" + } + }, + "aws_iam_role_policy_attachment": { + "testPocketApp_ecs_service_ecs_codedeploy_ecs_codedeploy_role_attachment_F9A8369F": { + "depends_on": [ + "aws_iam_role.testPocketApp_ecs_service_ecs_codedeploy_ecs_code_deploy_role_287C837D" + ], + "policy_arn": "arn:aws:iam::aws:policy/AWSCodeDeployRoleForECS", + "role": "\${aws_iam_role.testPocketApp_ecs_service_ecs_codedeploy_ecs_code_deploy_role_287C837D.name}" + } + }, + "aws_route53_record": { + "testPocketApp_alb_certificate_certificate_record_3C49201F": { + "depends_on": [ + "aws_acm_certificate.testPocketApp_alb_certificate_417C14FF" + ], + "name": "\${tolist(aws_acm_certificate.testPocketApp_alb_certificate_417C14FF.domain_validation_options).0.resource_record_name}", + "records": [ + "\${tolist(aws_acm_certificate.testPocketApp_alb_certificate_417C14FF.domain_validation_options).0.resource_record_value}" + ], + "ttl": 60, + "type": "\${tolist(aws_acm_certificate.testPocketApp_alb_certificate_417C14FF.domain_validation_options).0.resource_record_type}", + "zone_id": "\${aws_route53_zone.testPocketApp_base_dns_subhosted_zone_1E1E3243.zone_id}" + }, + "testPocketApp_alb_record_7B56ED33": { + "alias": { + "evaluate_target_health": true, + "name": "\${aws_alb.testPocketApp_application_load_balancer_alb_D538DEEE.dns_name}", + "zone_id": "\${aws_alb.testPocketApp_application_load_balancer_alb_D538DEEE.zone_id}" + }, + "lifecycle": { + "ignore_changes": [ + "weighted_routing_policy[0].weight" + ] + }, + "name": "testing.bowling.gov", + "set_identifier": "1", + "type": "A", + "weighted_routing_policy": { + "weight": 1 + }, + "zone_id": "\${aws_route53_zone.testPocketApp_base_dns_subhosted_zone_1E1E3243.zone_id}" + }, + "testPocketApp_base_dns_subhosted_zone_ns_497FFB83": { + "name": "testing.bowling.gov", + "records": "\${aws_route53_zone.testPocketApp_base_dns_subhosted_zone_1E1E3243.name_servers}", + "ttl": 86400, + "type": "NS", + "zone_id": "\${data.aws_route53_zone.testPocketApp_base_dns_main_hosted_zone_9442EEB0.zone_id}" + } + }, + "aws_route53_zone": { + "testPocketApp_base_dns_subhosted_zone_1E1E3243": { + "name": "testing.bowling.gov" + } + }, + "aws_security_group": { + "testPocketApp_application_load_balancer_alb_security_group_91A27046": { + "description": "External security group (Managed by Terraform)", + "egress": [ + { + "cidr_blocks": [ + "0.0.0.0/0" + ], + "description": "required", + "from_port": 0, + "ipv6_cidr_blocks": [ + ], + "prefix_list_ids": [ + ], + "protocol": "-1", + "security_groups": [ + ], + "self": null, + "to_port": 0 + } + ], + "ingress": [ + { + "cidr_blocks": [ + "0.0.0.0/0" + ], + "description": null, + "from_port": 443, + "ipv6_cidr_blocks": null, + "prefix_list_ids": null, + "protocol": "TCP", + "security_groups": null, + "self": null, + "to_port": 443 + }, + { + "cidr_blocks": [ + "0.0.0.0/0" + ], + "description": null, + "from_port": 80, + "ipv6_cidr_blocks": null, + "prefix_list_ids": null, + "protocol": "TCP", + "security_groups": null, + "self": null, + "to_port": 80 + } + ], + "lifecycle": { + "create_before_destroy": true + }, + "name_prefix": "testapp-HTTP/S Security Group", + "tags": { + "Name": "testapp-HTTP/S Security Group" + }, + "vpc_id": "\${data.aws_vpc.testPocketApp_pocket_vpc_C4E157E3.id}" + }, + "testPocketApp_ecs_service_ecs_security_group_9C016FA8": { + "description": "Internal ECS Security Group (Managed by Terraform)", + "egress": [ + { + "cidr_blocks": [ + "0.0.0.0/0" + ], + "description": "required", + "from_port": 0, + "ipv6_cidr_blocks": [ + ], + "prefix_list_ids": [ + ], + "protocol": "-1", + "security_groups": [ + ], + "self": null, + "to_port": 0 + } + ], + "ingress": [ + { + "cidr_blocks": [ + ], + "description": "required", + "from_port": 80, + "ipv6_cidr_blocks": [ + ], + "prefix_list_ids": [ + ], + "protocol": "TCP", + "security_groups": [ + "\${aws_security_group.testPocketApp_application_load_balancer_alb_security_group_91A27046.id}" + ], + "self": null, + "to_port": 8675309 + } + ], + "lifecycle": { + "create_before_destroy": true + }, + "name_prefix": "testapp-ECSSecurityGroup", + "vpc_id": "\${data.aws_vpc.testPocketApp_pocket_vpc_C4E157E3.id}" + } + }, + "local_file": { + "testPocketApp_ecs_service_appspec_6EB2FE4D": { + "content": "{\\"version\\":1,\\"Resources\\":[{\\"TargetService\\":{\\"Type\\":\\"AWS::ECS::Service\\",\\"Properties\\":{\\"TaskDefinition\\":\\"\${aws_ecs_task_definition.testPocketApp_ecs_service_ecs-task_A7E74E45.arn}\\",\\"LoadBalancerInfo\\":{\\"ContainerName\\":\\"main_container\\",\\"ContainerPort\\":8675309}}}}]}", + "filename": "appspec.json" + } + }, + "null_resource": { + "testPocketApp_ecs_service_create-task-definition-file_5769615D": { + "depends_on": [ + "aws_ecs_task_definition.testPocketApp_ecs_service_ecs-task_A7E74E45" + ], + "provisioner": { + "local-exec": { + "command": "aws --region us-east-1 ecs describe-task-definition --task-definition \${aws_ecs_task_definition.testPocketApp_ecs_service_ecs-task_A7E74E45.family} --query 'taskDefinition' >> taskdef.json" + } + }, + "triggers": { + "alwaysRun": "\${timestamp()}" + } + } + } + } +}" +`; + +exports[`PocketALBApplication renders an Pocket App with code deploy notifications set to failed only 1`] = ` +"{ + "data": { + "aws_caller_identity": { + "testPocketApp_pocket_vpc_current_identity_06D87057": { + } + }, + "aws_iam_policy_document": { + "testPocketApp_ecs_service_ecs-iam_ecs-task-assume_ABB81680": { + "statement": [ + { + "actions": [ + "sts:AssumeRole" + ], + "effect": "Allow", + "principals": [ + { + "identifiers": [ + "ecs-tasks.amazonaws.com" + ], + "type": "Service" + } + ] + } + ], + "version": "2012-10-17" + }, + "testPocketApp_ecs_service_ecs_codedeploy_codedeploy_assume_role_840D1ECF": { + "statement": [ + { + "actions": [ + "sts:AssumeRole" + ], + "effect": "Allow", + "principals": [ + { + "identifiers": [ + "codedeploy.amazonaws.com" + ], + "type": "Service" + } + ] + } + ] + } + }, + "aws_kms_alias": { + "testPocketApp_pocket_vpc_secrets_manager_key_9FD2BC93": { + "name": "alias/aws/secretsmanager" + } + }, + "aws_region": { + "testPocketApp_pocket_vpc_current_region_8ED435E7": { + } + }, + "aws_route53_zone": { + "testPocketApp_base_dns_main_hosted_zone_9442EEB0": { + "name": "bowling.gov" + } + }, + "aws_security_groups": { + "testPocketApp_pocket_vpc_default_security_groups_40B4FC48": { + "filter": [ + { + "name": "group-name", + "values": [ + "default" + ] + }, + { + "name": "vpc-id", + "values": [ + "\${data.aws_vpc.testPocketApp_pocket_vpc_C4E157E3.id}" + ] + } + ] + }, + "testPocketApp_pocket_vpc_internal_security_groups_12F7F666": { + "filter": [ + { + "name": "group-name", + "values": [ + "pocket-vpc-internal" + ] + }, + { + "name": "vpc-id", + "values": [ + "\${data.aws_vpc.testPocketApp_pocket_vpc_C4E157E3.id}" + ] + } + ] + } + }, + "aws_ssm_parameter": { + "testPocketApp_pocket_vpc_private_subnets_9D449563": { + "name": "/Shared/PrivateSubnets" + }, + "testPocketApp_pocket_vpc_public_subnets_B0B3A6AB": { + "name": "/Shared/PublicSubnets" + }, + "testPocketApp_pocket_vpc_vpc_ssm_param_235525D4": { + "name": "/Shared/Vpc" + } + }, + "aws_subnets": { + "testPocketApp_pocket_vpc_private_subnet_ids_EB1E3A65": { + "filter": [ + { + "name": "subnet-id", + "values": "\${split(\\",\\", data.aws_ssm_parameter.testPocketApp_pocket_vpc_private_subnets_9D449563.value)}" + }, + { + "name": "vpc-id", + "values": [ + "\${data.aws_vpc.testPocketApp_pocket_vpc_C4E157E3.id}" + ] + } + ] + }, + "testPocketApp_pocket_vpc_public_subnet_ids_01F0B902": { + "filter": [ + { + "name": "subnet-id", + "values": "\${split(\\",\\", data.aws_ssm_parameter.testPocketApp_pocket_vpc_public_subnets_B0B3A6AB.value)}" + }, + { + "name": "vpc-id", + "values": [ + "\${data.aws_vpc.testPocketApp_pocket_vpc_C4E157E3.id}" + ] + } + ] + } + }, + "aws_vpc": { + "testPocketApp_pocket_vpc_C4E157E3": { + "filter": [ + { + "name": "vpc-id", + "values": [ + "\${data.aws_ssm_parameter.testPocketApp_pocket_vpc_vpc_ssm_param_235525D4.value}" + ] + } + ] + } + } + }, + "resource": { + "aws_acm_certificate": { + "testPocketApp_alb_certificate_417C14FF": { + "domain_name": "testing.bowling.gov", + "lifecycle": { + "create_before_destroy": true + }, + "validation_method": "DNS" + } + }, + "aws_acm_certificate_validation": { + "testPocketApp_alb_certificate_certificate_validation_7D899C6A": { + "certificate_arn": "\${aws_acm_certificate.testPocketApp_alb_certificate_417C14FF.arn}", + "depends_on": [ + "aws_route53_record.testPocketApp_alb_certificate_certificate_record_3C49201F", + "aws_acm_certificate.testPocketApp_alb_certificate_417C14FF" + ], + "validation_record_fqdns": [ + "\${aws_route53_record.testPocketApp_alb_certificate_certificate_record_3C49201F.fqdn}" + ] + } + }, + "aws_alb": { + "testPocketApp_application_load_balancer_alb_D538DEEE": { + "internal": false, + "name_prefix": "TSTAPP", + "security_groups": [ + "\${aws_security_group.testPocketApp_application_load_balancer_alb_security_group_91A27046.id}" + ], + "subnets": "\${data.aws_subnets.testPocketApp_pocket_vpc_public_subnet_ids_01F0B902.ids}" + } + }, + "aws_alb_listener": { + "testPocketApp_listener_http_A9E227C2": { + "default_action": [ + { + "redirect": { + "port": "443", + "protocol": "HTTPS", + "status_code": "HTTP_301" + }, + "type": "redirect" + } + ], + "load_balancer_arn": "\${aws_alb.testPocketApp_application_load_balancer_alb_D538DEEE.arn}", + "port": 80, + "protocol": "HTTP" + }, + "testPocketApp_listener_https_7F49DE83": { + "certificate_arn": "\${aws_acm_certificate.testPocketApp_alb_certificate_417C14FF.arn}", + "default_action": [ + { + "fixed_response": { + "content_type": "text/plain", + "message_body": "", + "status_code": "503" + }, + "type": "fixed-response" + } + ], + "load_balancer_arn": "\${aws_alb.testPocketApp_application_load_balancer_alb_D538DEEE.arn}", + "port": 443, + "protocol": "HTTPS", + "ssl_policy": "ELBSecurityPolicy-TLS-1-1-2017-01" + } + }, + "aws_alb_listener_rule": { + "testPocketApp_ecs_service_listener_rule_C03BB04D": { + "action": [ + { + "target_group_arn": "\${aws_alb_target_group.testPocketApp_ecs_service_blue_target_group_ecs_target_group_A4B85885.arn}", + "type": "forward" + } + ], + "condition": [ + { + "path_pattern": { + "values": [ + "*" + ] + } + } + ], + "lifecycle": { + "ignore_changes": [ + "action" + ] + }, + "listener_arn": "\${aws_alb_listener.testPocketApp_listener_https_7F49DE83.arn}", + "priority": 1 + } + }, + "aws_alb_target_group": { + "testPocketApp_ecs_service_blue_target_group_ecs_target_group_A4B85885": { + "deregistration_delay": "120", + "health_check": { + "healthy_threshold": 5, + "interval": 15, + "path": "/test", + "protocol": "HTTP", + "unhealthy_threshold": 3 + }, + "name_prefix": "TSTAPP", + "port": 80, + "protocol": "HTTP", + "tags": { + "type": "blue" + }, + "target_type": "ip", + "vpc_id": "\${data.aws_vpc.testPocketApp_pocket_vpc_C4E157E3.id}" + }, + "testPocketApp_ecs_service_green_target_group_ecs_target_group_0E777BA1": { + "deregistration_delay": "120", + "health_check": { + "healthy_threshold": 5, + "interval": 15, + "path": "/test", + "protocol": "HTTP", + "unhealthy_threshold": 3 + }, + "name_prefix": "TSTAPP", + "port": 80, + "protocol": "HTTP", + "tags": { + "type": "green" + }, + "target_type": "ip", + "vpc_id": "\${data.aws_vpc.testPocketApp_pocket_vpc_C4E157E3.id}" + } + }, + "aws_appautoscaling_policy": { + "testPocketApp_autoscaling_scale_in_policy_A163A235": { + "depends_on": [ + "aws_appautoscaling_target.testPocketApp_autoscaling_autoscaling_target_505E4EA1" + ], + "name": "testapp-ScaleInPolicy", + "policy_type": "StepScaling", + "resource_id": "\${aws_appautoscaling_target.testPocketApp_autoscaling_autoscaling_target_505E4EA1.resource_id}", + "scalable_dimension": "\${aws_appautoscaling_target.testPocketApp_autoscaling_autoscaling_target_505E4EA1.scalable_dimension}", + "service_namespace": "\${aws_appautoscaling_target.testPocketApp_autoscaling_autoscaling_target_505E4EA1.service_namespace}", + "step_scaling_policy_configuration": { + "adjustment_type": "ChangeInCapacity", + "cooldown": 60, + "metric_aggregation_type": "Average", + "step_adjustment": [ + { + "metric_interval_upper_bound": "0", + "scaling_adjustment": -1 + } + ] + }, + "target_tracking_scaling_policy_configuration": null + }, + "testPocketApp_autoscaling_scale_out_policy_075BFAC4": { + "depends_on": [ + "aws_appautoscaling_target.testPocketApp_autoscaling_autoscaling_target_505E4EA1" + ], + "name": "testapp-ScaleOutPolicy", + "policy_type": "StepScaling", + "resource_id": "\${aws_appautoscaling_target.testPocketApp_autoscaling_autoscaling_target_505E4EA1.resource_id}", + "scalable_dimension": "\${aws_appautoscaling_target.testPocketApp_autoscaling_autoscaling_target_505E4EA1.scalable_dimension}", + "service_namespace": "\${aws_appautoscaling_target.testPocketApp_autoscaling_autoscaling_target_505E4EA1.service_namespace}", + "step_scaling_policy_configuration": { + "adjustment_type": "ChangeInCapacity", + "cooldown": 60, + "metric_aggregation_type": "Average", + "step_adjustment": [ + { + "metric_interval_lower_bound": "0", + "scaling_adjustment": 2 + } + ] + }, + "target_tracking_scaling_policy_configuration": null + } + }, + "aws_appautoscaling_target": { + "testPocketApp_autoscaling_autoscaling_target_505E4EA1": { + "max_capacity": 2, + "min_capacity": 1, + "resource_id": "service/\${aws_ecs_cluster.testPocketApp_ecs_cluster_C3960066.name}/\${aws_ecs_service.testPocketApp_ecs_service_ecs-service_182DEA4C.name}", + "scalable_dimension": "ecs:service:DesiredCount", + "service_namespace": "ecs" + } + }, + "aws_cloudwatch_dashboard": { + "testPocketApp_cloudwatch-dashboard_26E9F70C": { + "dashboard_body": "{\\"widgets\\":[{\\"type\\":\\"metric\\",\\"x\\":0,\\"y\\":0,\\"width\\":12,\\"height\\":6,\\"properties\\":{\\"metrics\\":[[\\"AWS/ApplicationELB\\",\\"HTTPCode_Target_4XX_Count\\",\\"LoadBalancer\\",\\"\${aws_alb.testPocketApp_application_load_balancer_alb_D538DEEE.arn_suffix}\\",{\\"yAxis\\":\\"left\\",\\"color\\":\\"#ff7f0e\\"}],[\\".\\",\\"RequestCount\\",\\".\\",\\".\\",{\\"yAxis\\":\\"right\\",\\"color\\":\\"#1f77b4\\"}],[\\".\\",\\"HTTPCode_Target_5XX_Count\\",\\".\\",\\".\\",{\\"color\\":\\"#d62728\\"}],[\\".\\",\\"HTTPCode_Target_2XX_Count\\",\\".\\",\\".\\",{\\"yAxis\\":\\"right\\",\\"color\\":\\"#2ca02c\\"}]],\\"view\\":\\"timeSeries\\",\\"stacked\\":false,\\"region\\":\\"us-east-1\\",\\"period\\":60,\\"stat\\":\\"Sum\\",\\"title\\":\\"Target Requests\\"}},{\\"type\\":\\"metric\\",\\"x\\":12,\\"y\\":0,\\"width\\":12,\\"height\\":6,\\"properties\\":{\\"metrics\\":[[\\"AWS/ApplicationELB\\",\\"HTTPCode_ELB_4XX_Count\\",\\"LoadBalancer\\",\\"\${aws_alb.testPocketApp_application_load_balancer_alb_D538DEEE.arn_suffix}\\",{\\"yAxis\\":\\"left\\",\\"color\\":\\"#ff7f0e\\"}],[\\".\\",\\"RequestCount\\",\\".\\",\\".\\",{\\"yAxis\\":\\"right\\",\\"color\\":\\"#1f77b4\\"}],[\\".\\",\\"HTTPCode_ELB_5XX_Count\\",\\".\\",\\".\\",{\\"color\\":\\"#d62728\\"}]],\\"view\\":\\"timeSeries\\",\\"stacked\\":false,\\"region\\":\\"us-east-1\\",\\"period\\":60,\\"stat\\":\\"Sum\\",\\"title\\":\\"ALB Requests\\"}},{\\"type\\":\\"metric\\",\\"x\\":12,\\"y\\":6,\\"width\\":12,\\"height\\":6,\\"properties\\":{\\"metrics\\":[[\\"AWS/ApplicationELB\\",\\"TargetResponseTime\\",\\"LoadBalancer\\",\\"\${aws_alb.testPocketApp_application_load_balancer_alb_D538DEEE.arn_suffix}\\",{\\"label\\":\\"Average\\",\\"color\\":\\"#aec7e8\\"}],[\\"...\\",{\\"stat\\":\\"p95\\",\\"label\\":\\"p95\\",\\"color\\":\\"#ffbb78\\"}],[\\"...\\",{\\"stat\\":\\"p99\\",\\"label\\":\\"p99\\",\\"color\\":\\"#98df8a\\"}]],\\"view\\":\\"timeSeries\\",\\"stacked\\":false,\\"region\\":\\"us-east-1\\",\\"stat\\":\\"Average\\",\\"period\\":60}},{\\"type\\":\\"metric\\",\\"x\\":0,\\"y\\":6,\\"width\\":12,\\"height\\":6,\\"properties\\":{\\"metrics\\":[[\\"ECS/ContainerInsights\\",\\"RunningTaskCount\\",\\"ServiceName\\",\\"\${aws_ecs_service.testPocketApp_ecs_service_ecs-service_182DEA4C.name}\\",\\"ClusterName\\",\\"\${aws_ecs_cluster.testPocketApp_ecs_cluster_C3960066.name}\\",{\\"yAxis\\":\\"right\\",\\"color\\":\\"#c49c94\\"}],[\\"AWS/ECS\\",\\"CPUUtilization\\",\\".\\",\\".\\",\\".\\",\\".\\",{\\"color\\":\\"#f7b6d2\\"}],[\\".\\",\\"MemoryUtilization\\",\\".\\",\\".\\",\\".\\",\\".\\",{\\"color\\":\\"#c7c7c7\\"}]],\\"view\\":\\"timeSeries\\",\\"stacked\\":false,\\"region\\":\\"us-east-1\\",\\"stat\\":\\"Average\\",\\"period\\":60,\\"annotations\\":{\\"horizontal\\":[{\\"color\\":\\"#e377c2\\",\\"label\\":\\"CPU scale out\\",\\"value\\":45},{\\"color\\":\\"#c5b0d5\\",\\"label\\":\\"CPU scale in\\",\\"value\\":30}]},\\"title\\":\\"Service Load\\"}}]}", + "dashboard_name": "testapp", + "lifecycle": { + "ignore_changes": [ + "dashboard_body" + ] + } + } + }, + "aws_cloudwatch_metric_alarm": { + "testPocketApp_autoscaling_scale_in_alarm_69B5F00D": { + "alarm_actions": [ + "\${aws_appautoscaling_policy.testPocketApp_autoscaling_scale_in_policy_A163A235.arn}" + ], + "alarm_description": "Alarm to reduce capacity if container CPU is low", + "alarm_name": "testapp Service Low CPU", + "comparison_operator": "LessThanThreshold", + "dimensions": { + "ClusterName": "\${aws_ecs_cluster.testPocketApp_ecs_cluster_C3960066.name}", + "ServiceName": "\${aws_ecs_service.testPocketApp_ecs_service_ecs-service_182DEA4C.name}" + }, + "evaluation_periods": 2, + "metric_name": "CPUUtilization", + "namespace": "AWS/ECS", + "period": 60, + "statistic": "Average", + "threshold": 30, + "treat_missing_data": "notBreaching" + }, + "testPocketApp_autoscaling_scale_out_alarm_4313FBE9": { + "alarm_actions": [ + "\${aws_appautoscaling_policy.testPocketApp_autoscaling_scale_out_policy_075BFAC4.arn}" + ], + "alarm_description": "Alarm to add capacity if container CPU is high", + "alarm_name": "testapp Service High CPU", + "comparison_operator": "GreaterThanThreshold", + "dimensions": { + "ClusterName": "\${aws_ecs_cluster.testPocketApp_ecs_cluster_C3960066.name}", + "ServiceName": "\${aws_ecs_service.testPocketApp_ecs_service_ecs-service_182DEA4C.name}" + }, + "evaluation_periods": 2, + "metric_name": "CPUUtilization", + "namespace": "AWS/ECS", + "period": 60, + "statistic": "Average", + "threshold": 45, + "treat_missing_data": "notBreaching" + } + }, + "aws_codedeploy_app": { + "testPocketApp_ecs_service_ecs_codedeploy_ecs_code_deploy_480D0565": { + "compute_platform": "ECS", + "name": "testapp-ECS" + } + }, + "aws_codedeploy_deployment_group": { + "testPocketApp_ecs_service_ecs_codedeploy_ecs_codedeploy_deployment_group_44B006D1": { + "app_name": "\${aws_codedeploy_app.testPocketApp_ecs_service_ecs_codedeploy_ecs_code_deploy_480D0565.name}", + "auto_rollback_configuration": { + "enabled": true, + "events": [ + "DEPLOYMENT_FAILURE" + ] + }, + "blue_green_deployment_config": { + "deployment_ready_option": { + "action_on_timeout": "CONTINUE_DEPLOYMENT" + }, + "terminate_blue_instances_on_deployment_success": { + "action": "TERMINATE", + "termination_wait_time_in_minutes": 5 + } + }, + "depends_on": [ + "aws_ecs_service.testPocketApp_ecs_service_ecs-service_182DEA4C" + ], + "deployment_config_name": "CodeDeployDefault.ECSAllAtOnce", + "deployment_group_name": "testapp-ECS", + "deployment_style": { + "deployment_option": "WITH_TRAFFIC_CONTROL", + "deployment_type": "BLUE_GREEN" + }, + "ecs_service": { + "cluster_name": "\${aws_ecs_cluster.testPocketApp_ecs_cluster_C3960066.name}", + "service_name": "\${aws_ecs_service.testPocketApp_ecs_service_ecs-service_182DEA4C.name}" + }, + "load_balancer_info": { + "target_group_pair_info": { + "prod_traffic_route": { + "listener_arns": [ + "\${aws_alb_listener.testPocketApp_listener_https_7F49DE83.arn}" + ] + }, + "target_group": [ + { + "name": "\${aws_alb_target_group.testPocketApp_ecs_service_blue_target_group_ecs_target_group_A4B85885.name}" + }, + { + "name": "\${aws_alb_target_group.testPocketApp_ecs_service_green_target_group_ecs_target_group_0E777BA1.name}" + } + ] + } + }, + "service_role_arn": "\${aws_iam_role.testPocketApp_ecs_service_ecs_codedeploy_ecs_code_deploy_role_287C837D.arn}" + } + }, + "aws_ecs_cluster": { + "testPocketApp_ecs_cluster_C3960066": { + "name": "testapp", + "setting": [ + { + "name": "containerInsights", + "value": "enabled" + } + ] + } + }, + "aws_ecs_service": { + "testPocketApp_ecs_service_ecs-service_182DEA4C": { + "cluster": "\${aws_ecs_cluster.testPocketApp_ecs_cluster_C3960066.arn}", + "depends_on": [ + "aws_alb_target_group.testPocketApp_ecs_service_blue_target_group_ecs_target_group_A4B85885", + "aws_alb_listener_rule.testPocketApp_ecs_service_listener_rule_C03BB04D" + ], + "deployment_controller": { + "type": "CODE_DEPLOY" + }, + "deployment_maximum_percent": 200, + "deployment_minimum_healthy_percent": 100, + "desired_count": 2, + "launch_type": "FARGATE", + "lifecycle": { + "ignore_changes": [ + "desired_count", + "load_balancer", + "task_definition" + ] + }, + "load_balancer": [ + { + "container_name": "main_container", + "container_port": 8675309, + "target_group_arn": "\${aws_alb_target_group.testPocketApp_ecs_service_blue_target_group_ecs_target_group_A4B85885.arn}" + } + ], + "name": "testapp", + "network_configuration": { + "security_groups": [ + "\${aws_security_group.testPocketApp_ecs_service_ecs_security_group_9C016FA8.id}" + ], + "subnets": "\${data.aws_subnets.testPocketApp_pocket_vpc_private_subnet_ids_EB1E3A65.ids}" + }, + "propagate_tags": "SERVICE", + "task_definition": "\${aws_ecs_task_definition.testPocketApp_ecs_service_ecs-task_A7E74E45.arn}" + } + }, + "aws_ecs_task_definition": { + "testPocketApp_ecs_service_ecs-task_A7E74E45": { + "container_definitions": "[]", + "cpu": "512", + "depends_on": [ + ], + "execution_role_arn": "\${aws_iam_role.testPocketApp_ecs_service_ecs-iam_ecs-execution-role_EB461A0C.arn}", + "family": "testapp", + "memory": "2048", + "network_mode": "awsvpc", + "requires_compatibilities": [ + "FARGATE" + ], + "task_role_arn": "\${aws_iam_role.testPocketApp_ecs_service_ecs-iam_ecs-task-role_7DB93963.arn}", + "volume": [ + ] + } + }, + "aws_iam_role": { + "testPocketApp_ecs_service_ecs-iam_ecs-execution-role_EB461A0C": { + "assume_role_policy": "\${data.aws_iam_policy_document.testPocketApp_ecs_service_ecs-iam_ecs-task-assume_ABB81680.json}", + "name": "testapp-TaskExecutionRole" + }, + "testPocketApp_ecs_service_ecs-iam_ecs-task-role_7DB93963": { + "assume_role_policy": "\${data.aws_iam_policy_document.testPocketApp_ecs_service_ecs-iam_ecs-task-assume_ABB81680.json}", + "name": "testapp-TaskRole" + }, + "testPocketApp_ecs_service_ecs_codedeploy_ecs_code_deploy_role_287C837D": { + "assume_role_policy": "\${data.aws_iam_policy_document.testPocketApp_ecs_service_ecs_codedeploy_codedeploy_assume_role_840D1ECF.json}", + "name": "testapp-ECSCodeDeployRole" + } + }, + "aws_iam_role_policy_attachment": { + "testPocketApp_ecs_service_ecs_codedeploy_ecs_codedeploy_role_attachment_F9A8369F": { + "depends_on": [ + "aws_iam_role.testPocketApp_ecs_service_ecs_codedeploy_ecs_code_deploy_role_287C837D" + ], + "policy_arn": "arn:aws:iam::aws:policy/AWSCodeDeployRoleForECS", + "role": "\${aws_iam_role.testPocketApp_ecs_service_ecs_codedeploy_ecs_code_deploy_role_287C837D.name}" + } + }, + "aws_route53_record": { + "testPocketApp_alb_certificate_certificate_record_3C49201F": { + "depends_on": [ + "aws_acm_certificate.testPocketApp_alb_certificate_417C14FF" + ], + "name": "\${tolist(aws_acm_certificate.testPocketApp_alb_certificate_417C14FF.domain_validation_options).0.resource_record_name}", + "records": [ + "\${tolist(aws_acm_certificate.testPocketApp_alb_certificate_417C14FF.domain_validation_options).0.resource_record_value}" + ], + "ttl": 60, + "type": "\${tolist(aws_acm_certificate.testPocketApp_alb_certificate_417C14FF.domain_validation_options).0.resource_record_type}", + "zone_id": "\${aws_route53_zone.testPocketApp_base_dns_subhosted_zone_1E1E3243.zone_id}" + }, + "testPocketApp_alb_record_7B56ED33": { + "alias": { + "evaluate_target_health": true, + "name": "\${aws_alb.testPocketApp_application_load_balancer_alb_D538DEEE.dns_name}", + "zone_id": "\${aws_alb.testPocketApp_application_load_balancer_alb_D538DEEE.zone_id}" + }, + "lifecycle": { + "ignore_changes": [ + "weighted_routing_policy[0].weight" + ] + }, + "name": "testing.bowling.gov", + "set_identifier": "1", + "type": "A", + "weighted_routing_policy": { + "weight": 1 + }, + "zone_id": "\${aws_route53_zone.testPocketApp_base_dns_subhosted_zone_1E1E3243.zone_id}" + }, + "testPocketApp_base_dns_subhosted_zone_ns_497FFB83": { + "name": "testing.bowling.gov", + "records": "\${aws_route53_zone.testPocketApp_base_dns_subhosted_zone_1E1E3243.name_servers}", + "ttl": 86400, + "type": "NS", + "zone_id": "\${data.aws_route53_zone.testPocketApp_base_dns_main_hosted_zone_9442EEB0.zone_id}" + } + }, + "aws_route53_zone": { + "testPocketApp_base_dns_subhosted_zone_1E1E3243": { + "name": "testing.bowling.gov" + } + }, + "aws_security_group": { + "testPocketApp_application_load_balancer_alb_security_group_91A27046": { + "description": "External security group (Managed by Terraform)", + "egress": [ + { + "cidr_blocks": [ + "0.0.0.0/0" + ], + "description": "required", + "from_port": 0, + "ipv6_cidr_blocks": [ + ], + "prefix_list_ids": [ + ], + "protocol": "-1", + "security_groups": [ + ], + "self": null, + "to_port": 0 + } + ], + "ingress": [ + { + "cidr_blocks": [ + "0.0.0.0/0" + ], + "description": null, + "from_port": 443, + "ipv6_cidr_blocks": null, + "prefix_list_ids": null, + "protocol": "TCP", + "security_groups": null, + "self": null, + "to_port": 443 + }, + { + "cidr_blocks": [ + "0.0.0.0/0" + ], + "description": null, + "from_port": 80, + "ipv6_cidr_blocks": null, + "prefix_list_ids": null, + "protocol": "TCP", + "security_groups": null, + "self": null, + "to_port": 80 + } + ], + "lifecycle": { + "create_before_destroy": true + }, + "name_prefix": "testapp-HTTP/S Security Group", + "tags": { + "Name": "testapp-HTTP/S Security Group" + }, + "vpc_id": "\${data.aws_vpc.testPocketApp_pocket_vpc_C4E157E3.id}" + }, + "testPocketApp_ecs_service_ecs_security_group_9C016FA8": { + "description": "Internal ECS Security Group (Managed by Terraform)", + "egress": [ + { + "cidr_blocks": [ + "0.0.0.0/0" + ], + "description": "required", + "from_port": 0, + "ipv6_cidr_blocks": [ + ], + "prefix_list_ids": [ + ], + "protocol": "-1", + "security_groups": [ + ], + "self": null, + "to_port": 0 + } + ], + "ingress": [ + { + "cidr_blocks": [ + ], + "description": "required", + "from_port": 80, + "ipv6_cidr_blocks": [ + ], + "prefix_list_ids": [ + ], + "protocol": "TCP", + "security_groups": [ + "\${aws_security_group.testPocketApp_application_load_balancer_alb_security_group_91A27046.id}" + ], + "self": null, + "to_port": 8675309 + } + ], + "lifecycle": { + "create_before_destroy": true + }, + "name_prefix": "testapp-ECSSecurityGroup", + "vpc_id": "\${data.aws_vpc.testPocketApp_pocket_vpc_C4E157E3.id}" + } + }, + "local_file": { + "testPocketApp_ecs_service_appspec_6EB2FE4D": { + "content": "{\\"version\\":1,\\"Resources\\":[{\\"TargetService\\":{\\"Type\\":\\"AWS::ECS::Service\\",\\"Properties\\":{\\"TaskDefinition\\":\\"\${aws_ecs_task_definition.testPocketApp_ecs_service_ecs-task_A7E74E45.arn}\\",\\"LoadBalancerInfo\\":{\\"ContainerName\\":\\"main_container\\",\\"ContainerPort\\":8675309}}}}]}", + "filename": "appspec.json" + } + }, + "null_resource": { + "testPocketApp_ecs_service_update-task-definition_23CCF5D1": { + "depends_on": [ + "aws_ecs_task_definition.testPocketApp_ecs_service_ecs-task_A7E74E45", + "aws_codedeploy_app.testPocketApp_ecs_service_ecs_codedeploy_ecs_code_deploy_480D0565", + "aws_codedeploy_deployment_group.testPocketApp_ecs_service_ecs_codedeploy_ecs_codedeploy_deployment_group_44B006D1" + ], + "provisioner": { + "local-exec": { + "command": "export app_spec_content_string='{\\"version\\":1,\\"Resources\\":[{\\"TargetService\\":{\\"Type\\":\\"AWS::ECS::Service\\",\\"Properties\\":{\\"TaskDefinition\\":\\"\${aws_ecs_task_definition.testPocketApp_ecs_service_ecs-task_A7E74E45.arn}\\",\\"LoadBalancerInfo\\":{\\"ContainerName\\":\\"main_container\\",\\"ContainerPort\\":8675309}}}}]}' && export revision=\\"revisionType=AppSpecContent,appSpecContent={content='$app_spec_content_string'}\\" && aws --region us-east-1 deploy create-deployment --application-name=\\"\${aws_codedeploy_app.testPocketApp_ecs_service_ecs_codedeploy_ecs_code_deploy_480D0565.name}\\" --deployment-group-name=\\"\${aws_codedeploy_deployment_group.testPocketApp_ecs_service_ecs_codedeploy_ecs_codedeploy_deployment_group_44B006D1.deployment_group_name}\\" --description=\\"Triggered from Terraform/CodeBuild due to a task definition update\\" --revision=\\"$revision\\"" + } + }, + "triggers": { + "task_arn": "\${aws_ecs_task_definition.testPocketApp_ecs_service_ecs-task_A7E74E45.arn}" + } + } + } + } +}" +`; + +exports[`PocketALBApplication renders an Pocket App with custom Alarm Description 1`] = ` +"{ + "data": { + "aws_caller_identity": { + "testPocketApp_pocket_vpc_current_identity_06D87057": { + } + }, + "aws_iam_policy_document": { + "testPocketApp_ecs_service_ecs-iam_ecs-task-assume_ABB81680": { + "statement": [ + { + "actions": [ + "sts:AssumeRole" + ], + "effect": "Allow", + "principals": [ + { + "identifiers": [ + "ecs-tasks.amazonaws.com" + ], + "type": "Service" + } + ] + } + ], + "version": "2012-10-17" + } + }, + "aws_kms_alias": { + "testPocketApp_pocket_vpc_secrets_manager_key_9FD2BC93": { + "name": "alias/aws/secretsmanager" + } + }, + "aws_region": { + "testPocketApp_pocket_vpc_current_region_8ED435E7": { + } + }, + "aws_route53_zone": { + "testPocketApp_base_dns_main_hosted_zone_9442EEB0": { + "name": "bowling.gov" + } + }, + "aws_security_groups": { + "testPocketApp_pocket_vpc_default_security_groups_40B4FC48": { + "filter": [ + { + "name": "group-name", + "values": [ + "default" + ] + }, + { + "name": "vpc-id", + "values": [ + "\${data.aws_vpc.testPocketApp_pocket_vpc_C4E157E3.id}" + ] + } + ] + }, + "testPocketApp_pocket_vpc_internal_security_groups_12F7F666": { + "filter": [ + { + "name": "group-name", + "values": [ + "pocket-vpc-internal" + ] + }, + { + "name": "vpc-id", + "values": [ + "\${data.aws_vpc.testPocketApp_pocket_vpc_C4E157E3.id}" + ] + } + ] + } + }, + "aws_ssm_parameter": { + "testPocketApp_pocket_vpc_private_subnets_9D449563": { + "name": "/Shared/PrivateSubnets" + }, + "testPocketApp_pocket_vpc_public_subnets_B0B3A6AB": { + "name": "/Shared/PublicSubnets" + }, + "testPocketApp_pocket_vpc_vpc_ssm_param_235525D4": { + "name": "/Shared/Vpc" + } + }, + "aws_subnets": { + "testPocketApp_pocket_vpc_private_subnet_ids_EB1E3A65": { + "filter": [ + { + "name": "subnet-id", + "values": "\${split(\\",\\", data.aws_ssm_parameter.testPocketApp_pocket_vpc_private_subnets_9D449563.value)}" + }, + { + "name": "vpc-id", + "values": [ + "\${data.aws_vpc.testPocketApp_pocket_vpc_C4E157E3.id}" + ] + } + ] + }, + "testPocketApp_pocket_vpc_public_subnet_ids_01F0B902": { + "filter": [ + { + "name": "subnet-id", + "values": "\${split(\\",\\", data.aws_ssm_parameter.testPocketApp_pocket_vpc_public_subnets_B0B3A6AB.value)}" + }, + { + "name": "vpc-id", + "values": [ + "\${data.aws_vpc.testPocketApp_pocket_vpc_C4E157E3.id}" + ] + } + ] + } + }, + "aws_vpc": { + "testPocketApp_pocket_vpc_C4E157E3": { + "filter": [ + { + "name": "vpc-id", + "values": [ + "\${data.aws_ssm_parameter.testPocketApp_pocket_vpc_vpc_ssm_param_235525D4.value}" + ] + } + ] + } + } + }, + "resource": { + "aws_acm_certificate": { + "testPocketApp_alb_certificate_417C14FF": { + "domain_name": "testing.bowling.gov", + "lifecycle": { + "create_before_destroy": true + }, + "validation_method": "DNS" + } + }, + "aws_acm_certificate_validation": { + "testPocketApp_alb_certificate_certificate_validation_7D899C6A": { + "certificate_arn": "\${aws_acm_certificate.testPocketApp_alb_certificate_417C14FF.arn}", + "depends_on": [ + "aws_route53_record.testPocketApp_alb_certificate_certificate_record_3C49201F", + "aws_acm_certificate.testPocketApp_alb_certificate_417C14FF" + ], + "validation_record_fqdns": [ + "\${aws_route53_record.testPocketApp_alb_certificate_certificate_record_3C49201F.fqdn}" + ] + } + }, + "aws_alb": { + "testPocketApp_application_load_balancer_alb_D538DEEE": { + "internal": false, + "name_prefix": "TSTAPP", + "security_groups": [ + "\${aws_security_group.testPocketApp_application_load_balancer_alb_security_group_91A27046.id}" + ], + "subnets": "\${data.aws_subnets.testPocketApp_pocket_vpc_public_subnet_ids_01F0B902.ids}" + } + }, + "aws_alb_listener": { + "testPocketApp_listener_http_A9E227C2": { + "default_action": [ + { + "redirect": { + "port": "443", + "protocol": "HTTPS", + "status_code": "HTTP_301" + }, + "type": "redirect" + } + ], + "load_balancer_arn": "\${aws_alb.testPocketApp_application_load_balancer_alb_D538DEEE.arn}", + "port": 80, + "protocol": "HTTP" + }, + "testPocketApp_listener_https_7F49DE83": { + "certificate_arn": "\${aws_acm_certificate.testPocketApp_alb_certificate_417C14FF.arn}", + "default_action": [ + { + "fixed_response": { + "content_type": "text/plain", + "message_body": "", + "status_code": "503" + }, + "type": "fixed-response" + } + ], + "load_balancer_arn": "\${aws_alb.testPocketApp_application_load_balancer_alb_D538DEEE.arn}", + "port": 443, + "protocol": "HTTPS", + "ssl_policy": "ELBSecurityPolicy-TLS-1-1-2017-01" + } + }, + "aws_alb_listener_rule": { + "testPocketApp_ecs_service_listener_rule_C03BB04D": { + "action": [ + { + "target_group_arn": "\${aws_alb_target_group.testPocketApp_ecs_service_blue_target_group_ecs_target_group_A4B85885.arn}", + "type": "forward" + } + ], + "condition": [ + { + "path_pattern": { + "values": [ + "*" + ] + } + } + ], + "lifecycle": { + "ignore_changes": [ + "action" + ] + }, + "listener_arn": "\${aws_alb_listener.testPocketApp_listener_https_7F49DE83.arn}", + "priority": 1 + } + }, + "aws_alb_target_group": { + "testPocketApp_ecs_service_blue_target_group_ecs_target_group_A4B85885": { + "deregistration_delay": "120", + "health_check": { + "healthy_threshold": 5, + "interval": 15, + "path": "/test", + "protocol": "HTTP", + "unhealthy_threshold": 3 + }, + "name_prefix": "TSTAPP", + "port": 80, + "protocol": "HTTP", + "tags": { + "type": "blue" + }, + "target_type": "ip", + "vpc_id": "\${data.aws_vpc.testPocketApp_pocket_vpc_C4E157E3.id}" + } + }, + "aws_appautoscaling_policy": { + "testPocketApp_autoscaling_scale_in_policy_A163A235": { + "depends_on": [ + "aws_appautoscaling_target.testPocketApp_autoscaling_autoscaling_target_505E4EA1" + ], + "name": "testapp-ScaleInPolicy", + "policy_type": "StepScaling", + "resource_id": "\${aws_appautoscaling_target.testPocketApp_autoscaling_autoscaling_target_505E4EA1.resource_id}", + "scalable_dimension": "\${aws_appautoscaling_target.testPocketApp_autoscaling_autoscaling_target_505E4EA1.scalable_dimension}", + "service_namespace": "\${aws_appautoscaling_target.testPocketApp_autoscaling_autoscaling_target_505E4EA1.service_namespace}", + "step_scaling_policy_configuration": { + "adjustment_type": "ChangeInCapacity", + "cooldown": 60, + "metric_aggregation_type": "Average", + "step_adjustment": [ + { + "metric_interval_upper_bound": "0", + "scaling_adjustment": -1 + } + ] + }, + "target_tracking_scaling_policy_configuration": null + }, + "testPocketApp_autoscaling_scale_out_policy_075BFAC4": { + "depends_on": [ + "aws_appautoscaling_target.testPocketApp_autoscaling_autoscaling_target_505E4EA1" + ], + "name": "testapp-ScaleOutPolicy", + "policy_type": "StepScaling", + "resource_id": "\${aws_appautoscaling_target.testPocketApp_autoscaling_autoscaling_target_505E4EA1.resource_id}", + "scalable_dimension": "\${aws_appautoscaling_target.testPocketApp_autoscaling_autoscaling_target_505E4EA1.scalable_dimension}", + "service_namespace": "\${aws_appautoscaling_target.testPocketApp_autoscaling_autoscaling_target_505E4EA1.service_namespace}", + "step_scaling_policy_configuration": { + "adjustment_type": "ChangeInCapacity", + "cooldown": 60, + "metric_aggregation_type": "Average", + "step_adjustment": [ + { + "metric_interval_lower_bound": "0", + "scaling_adjustment": 2 + } + ] + }, + "target_tracking_scaling_policy_configuration": null + } + }, + "aws_appautoscaling_target": { + "testPocketApp_autoscaling_autoscaling_target_505E4EA1": { + "max_capacity": 2, + "min_capacity": 1, + "resource_id": "service/\${aws_ecs_cluster.testPocketApp_ecs_cluster_C3960066.name}/\${aws_ecs_service.testPocketApp_ecs_service_ecs-service_182DEA4C.name}", + "scalable_dimension": "ecs:service:DesiredCount", + "service_namespace": "ecs" + } + }, + "aws_cloudwatch_dashboard": { + "testPocketApp_cloudwatch-dashboard_26E9F70C": { + "dashboard_body": "{\\"widgets\\":[{\\"type\\":\\"metric\\",\\"x\\":0,\\"y\\":0,\\"width\\":12,\\"height\\":6,\\"properties\\":{\\"metrics\\":[[\\"AWS/ApplicationELB\\",\\"HTTPCode_Target_4XX_Count\\",\\"LoadBalancer\\",\\"\${aws_alb.testPocketApp_application_load_balancer_alb_D538DEEE.arn_suffix}\\",{\\"yAxis\\":\\"left\\",\\"color\\":\\"#ff7f0e\\"}],[\\".\\",\\"RequestCount\\",\\".\\",\\".\\",{\\"yAxis\\":\\"right\\",\\"color\\":\\"#1f77b4\\"}],[\\".\\",\\"HTTPCode_Target_5XX_Count\\",\\".\\",\\".\\",{\\"color\\":\\"#d62728\\"}],[\\".\\",\\"HTTPCode_Target_2XX_Count\\",\\".\\",\\".\\",{\\"yAxis\\":\\"right\\",\\"color\\":\\"#2ca02c\\"}]],\\"view\\":\\"timeSeries\\",\\"stacked\\":false,\\"region\\":\\"us-east-1\\",\\"period\\":60,\\"stat\\":\\"Sum\\",\\"title\\":\\"Target Requests\\"}},{\\"type\\":\\"metric\\",\\"x\\":12,\\"y\\":0,\\"width\\":12,\\"height\\":6,\\"properties\\":{\\"metrics\\":[[\\"AWS/ApplicationELB\\",\\"HTTPCode_ELB_4XX_Count\\",\\"LoadBalancer\\",\\"\${aws_alb.testPocketApp_application_load_balancer_alb_D538DEEE.arn_suffix}\\",{\\"yAxis\\":\\"left\\",\\"color\\":\\"#ff7f0e\\"}],[\\".\\",\\"RequestCount\\",\\".\\",\\".\\",{\\"yAxis\\":\\"right\\",\\"color\\":\\"#1f77b4\\"}],[\\".\\",\\"HTTPCode_ELB_5XX_Count\\",\\".\\",\\".\\",{\\"color\\":\\"#d62728\\"}]],\\"view\\":\\"timeSeries\\",\\"stacked\\":false,\\"region\\":\\"us-east-1\\",\\"period\\":60,\\"stat\\":\\"Sum\\",\\"title\\":\\"ALB Requests\\"}},{\\"type\\":\\"metric\\",\\"x\\":12,\\"y\\":6,\\"width\\":12,\\"height\\":6,\\"properties\\":{\\"metrics\\":[[\\"AWS/ApplicationELB\\",\\"TargetResponseTime\\",\\"LoadBalancer\\",\\"\${aws_alb.testPocketApp_application_load_balancer_alb_D538DEEE.arn_suffix}\\",{\\"label\\":\\"Average\\",\\"color\\":\\"#aec7e8\\"}],[\\"...\\",{\\"stat\\":\\"p95\\",\\"label\\":\\"p95\\",\\"color\\":\\"#ffbb78\\"}],[\\"...\\",{\\"stat\\":\\"p99\\",\\"label\\":\\"p99\\",\\"color\\":\\"#98df8a\\"}]],\\"view\\":\\"timeSeries\\",\\"stacked\\":false,\\"region\\":\\"us-east-1\\",\\"stat\\":\\"Average\\",\\"period\\":60}},{\\"type\\":\\"metric\\",\\"x\\":0,\\"y\\":6,\\"width\\":12,\\"height\\":6,\\"properties\\":{\\"metrics\\":[[\\"ECS/ContainerInsights\\",\\"RunningTaskCount\\",\\"ServiceName\\",\\"\${aws_ecs_service.testPocketApp_ecs_service_ecs-service_182DEA4C.name}\\",\\"ClusterName\\",\\"\${aws_ecs_cluster.testPocketApp_ecs_cluster_C3960066.name}\\",{\\"yAxis\\":\\"right\\",\\"color\\":\\"#c49c94\\"}],[\\"AWS/ECS\\",\\"CPUUtilization\\",\\".\\",\\".\\",\\".\\",\\".\\",{\\"color\\":\\"#f7b6d2\\"}],[\\".\\",\\"MemoryUtilization\\",\\".\\",\\".\\",\\".\\",\\".\\",{\\"color\\":\\"#c7c7c7\\"}]],\\"view\\":\\"timeSeries\\",\\"stacked\\":false,\\"region\\":\\"us-east-1\\",\\"stat\\":\\"Average\\",\\"period\\":60,\\"annotations\\":{\\"horizontal\\":[{\\"color\\":\\"#e377c2\\",\\"label\\":\\"CPU scale out\\",\\"value\\":45},{\\"color\\":\\"#c5b0d5\\",\\"label\\":\\"CPU scale in\\",\\"value\\":30}]},\\"title\\":\\"Service Load\\"}}]}", + "dashboard_name": "testapp", + "lifecycle": { + "ignore_changes": [ + "dashboard_body" + ] + } + } + }, + "aws_cloudwatch_metric_alarm": { + "testPocketApp_alarm-httpresponsetime_327A4315": { + "alarm_actions": [ + ], + "alarm_description": "Uh oh. This is very slow.", + "alarm_name": "testapp-Alarm-HTTPResponseTime", + "comparison_operator": "GreaterThanThreshold", + "datapoints_to_alarm": 1, + "dimensions": { + "LoadBalancer": "\${aws_alb.testPocketApp_application_load_balancer_alb_D538DEEE.arn_suffix}" + }, + "evaluation_periods": 1, + "insufficient_data_actions": [ + ], + "metric_name": "TargetResponseTime", + "namespace": "AWS/ApplicationELB", + "ok_actions": [ + ], + "period": 300, + "statistic": "Average", + "threshold": 300 + }, + "testPocketApp_alarm-httptarget5xxerrorrate_39F471E9": { + "alarm_actions": [ + ], + "alarm_description": "Uh oh. something bad happened.", + "alarm_name": "testapp-Alarm-HTTPTarget5xxErrorRate", + "comparison_operator": "GreaterThanOrEqualToThreshold", + "datapoints_to_alarm": 5, + "evaluation_periods": 5, + "insufficient_data_actions": [ + ], + "metric_query": [ + { + "id": "requests", + "metric": { + "dimensions": { + "LoadBalancer": "\${aws_alb.testPocketApp_application_load_balancer_alb_D538DEEE.arn_suffix}" + }, + "metric_name": "RequestCount", + "namespace": "AWS/ApplicationELB", + "period": 60, + "stat": "Sum", + "unit": "Count" + } + }, + { + "id": "errors", + "metric": { + "dimensions": { + "LoadBalancer": "\${aws_alb.testPocketApp_application_load_balancer_alb_D538DEEE.arn_suffix}" + }, + "metric_name": "HTTPCode_Target_5XX_Count", + "namespace": "AWS/ApplicationELB", + "period": 60, + "stat": "Sum", + "unit": "Count" + } + }, + { + "expression": "errors/requests*100", + "id": "expression", + "label": "HTTP 5xx Error Rate", + "return_data": true + } + ], + "ok_actions": [ + ], + "threshold": 5 + }, + "testPocketApp_autoscaling_scale_in_alarm_69B5F00D": { + "alarm_actions": [ + "\${aws_appautoscaling_policy.testPocketApp_autoscaling_scale_in_policy_A163A235.arn}" + ], + "alarm_description": "Alarm to reduce capacity if container CPU is low", + "alarm_name": "testapp Service Low CPU", + "comparison_operator": "LessThanThreshold", + "dimensions": { + "ClusterName": "\${aws_ecs_cluster.testPocketApp_ecs_cluster_C3960066.name}", + "ServiceName": "\${aws_ecs_service.testPocketApp_ecs_service_ecs-service_182DEA4C.name}" + }, + "evaluation_periods": 2, + "metric_name": "CPUUtilization", + "namespace": "AWS/ECS", + "period": 60, + "statistic": "Average", + "threshold": 30, + "treat_missing_data": "notBreaching" + }, + "testPocketApp_autoscaling_scale_out_alarm_4313FBE9": { + "alarm_actions": [ + "\${aws_appautoscaling_policy.testPocketApp_autoscaling_scale_out_policy_075BFAC4.arn}" + ], + "alarm_description": "Alarm to add capacity if container CPU is high", + "alarm_name": "testapp Service High CPU", + "comparison_operator": "GreaterThanThreshold", + "dimensions": { + "ClusterName": "\${aws_ecs_cluster.testPocketApp_ecs_cluster_C3960066.name}", + "ServiceName": "\${aws_ecs_service.testPocketApp_ecs_service_ecs-service_182DEA4C.name}" + }, + "evaluation_periods": 2, + "metric_name": "CPUUtilization", + "namespace": "AWS/ECS", + "period": 60, + "statistic": "Average", + "threshold": 45, + "treat_missing_data": "notBreaching" + } + }, + "aws_ecs_cluster": { + "testPocketApp_ecs_cluster_C3960066": { + "name": "testapp", + "setting": [ + { + "name": "containerInsights", + "value": "enabled" + } + ] + } + }, + "aws_ecs_service": { + "testPocketApp_ecs_service_ecs-service_182DEA4C": { + "cluster": "\${aws_ecs_cluster.testPocketApp_ecs_cluster_C3960066.arn}", + "depends_on": [ + "aws_alb_target_group.testPocketApp_ecs_service_blue_target_group_ecs_target_group_A4B85885", + "aws_alb_listener_rule.testPocketApp_ecs_service_listener_rule_C03BB04D" + ], + "deployment_controller": { + "type": "ECS" + }, + "deployment_maximum_percent": 200, + "deployment_minimum_healthy_percent": 100, + "desired_count": 2, + "launch_type": "FARGATE", + "lifecycle": { + "ignore_changes": [ + "desired_count", + "load_balancer" + ] + }, + "load_balancer": [ + { + "container_name": "main_container", + "container_port": 8675309, + "target_group_arn": "\${aws_alb_target_group.testPocketApp_ecs_service_blue_target_group_ecs_target_group_A4B85885.arn}" + } + ], + "name": "testapp", + "network_configuration": { + "security_groups": [ + "\${aws_security_group.testPocketApp_ecs_service_ecs_security_group_9C016FA8.id}" + ], + "subnets": "\${data.aws_subnets.testPocketApp_pocket_vpc_private_subnet_ids_EB1E3A65.ids}" + }, + "propagate_tags": "SERVICE", + "task_definition": "\${aws_ecs_task_definition.testPocketApp_ecs_service_ecs-task_A7E74E45.arn}" + } + }, + "aws_ecs_task_definition": { + "testPocketApp_ecs_service_ecs-task_A7E74E45": { + "container_definitions": "[]", + "cpu": "512", + "depends_on": [ + ], + "execution_role_arn": "\${aws_iam_role.testPocketApp_ecs_service_ecs-iam_ecs-execution-role_EB461A0C.arn}", + "family": "testapp", + "memory": "2048", + "network_mode": "awsvpc", + "requires_compatibilities": [ + "FARGATE" + ], + "task_role_arn": "\${aws_iam_role.testPocketApp_ecs_service_ecs-iam_ecs-task-role_7DB93963.arn}", + "volume": [ + ] + } + }, + "aws_iam_role": { + "testPocketApp_ecs_service_ecs-iam_ecs-execution-role_EB461A0C": { + "assume_role_policy": "\${data.aws_iam_policy_document.testPocketApp_ecs_service_ecs-iam_ecs-task-assume_ABB81680.json}", + "name": "testapp-TaskExecutionRole" + }, + "testPocketApp_ecs_service_ecs-iam_ecs-task-role_7DB93963": { + "assume_role_policy": "\${data.aws_iam_policy_document.testPocketApp_ecs_service_ecs-iam_ecs-task-assume_ABB81680.json}", + "name": "testapp-TaskRole" + } + }, + "aws_route53_record": { + "testPocketApp_alb_certificate_certificate_record_3C49201F": { + "depends_on": [ + "aws_acm_certificate.testPocketApp_alb_certificate_417C14FF" + ], + "name": "\${tolist(aws_acm_certificate.testPocketApp_alb_certificate_417C14FF.domain_validation_options).0.resource_record_name}", + "records": [ + "\${tolist(aws_acm_certificate.testPocketApp_alb_certificate_417C14FF.domain_validation_options).0.resource_record_value}" + ], + "ttl": 60, + "type": "\${tolist(aws_acm_certificate.testPocketApp_alb_certificate_417C14FF.domain_validation_options).0.resource_record_type}", + "zone_id": "\${aws_route53_zone.testPocketApp_base_dns_subhosted_zone_1E1E3243.zone_id}" + }, + "testPocketApp_alb_record_7B56ED33": { + "alias": { + "evaluate_target_health": true, + "name": "\${aws_alb.testPocketApp_application_load_balancer_alb_D538DEEE.dns_name}", + "zone_id": "\${aws_alb.testPocketApp_application_load_balancer_alb_D538DEEE.zone_id}" + }, + "lifecycle": { + "ignore_changes": [ + "weighted_routing_policy[0].weight" + ] + }, + "name": "testing.bowling.gov", + "set_identifier": "1", + "type": "A", + "weighted_routing_policy": { + "weight": 1 + }, + "zone_id": "\${aws_route53_zone.testPocketApp_base_dns_subhosted_zone_1E1E3243.zone_id}" + }, + "testPocketApp_base_dns_subhosted_zone_ns_497FFB83": { + "name": "testing.bowling.gov", + "records": "\${aws_route53_zone.testPocketApp_base_dns_subhosted_zone_1E1E3243.name_servers}", + "ttl": 86400, + "type": "NS", + "zone_id": "\${data.aws_route53_zone.testPocketApp_base_dns_main_hosted_zone_9442EEB0.zone_id}" + } + }, + "aws_route53_zone": { + "testPocketApp_base_dns_subhosted_zone_1E1E3243": { + "name": "testing.bowling.gov" + } + }, + "aws_security_group": { + "testPocketApp_application_load_balancer_alb_security_group_91A27046": { + "description": "External security group (Managed by Terraform)", + "egress": [ + { + "cidr_blocks": [ + "0.0.0.0/0" + ], + "description": "required", + "from_port": 0, + "ipv6_cidr_blocks": [ + ], + "prefix_list_ids": [ + ], + "protocol": "-1", + "security_groups": [ + ], + "self": null, + "to_port": 0 + } + ], + "ingress": [ + { + "cidr_blocks": [ + "0.0.0.0/0" + ], + "description": null, + "from_port": 443, + "ipv6_cidr_blocks": null, + "prefix_list_ids": null, + "protocol": "TCP", + "security_groups": null, + "self": null, + "to_port": 443 + }, + { + "cidr_blocks": [ + "0.0.0.0/0" + ], + "description": null, + "from_port": 80, + "ipv6_cidr_blocks": null, + "prefix_list_ids": null, + "protocol": "TCP", + "security_groups": null, + "self": null, + "to_port": 80 + } + ], + "lifecycle": { + "create_before_destroy": true + }, + "name_prefix": "testapp-HTTP/S Security Group", + "tags": { + "Name": "testapp-HTTP/S Security Group" + }, + "vpc_id": "\${data.aws_vpc.testPocketApp_pocket_vpc_C4E157E3.id}" + }, + "testPocketApp_ecs_service_ecs_security_group_9C016FA8": { + "description": "Internal ECS Security Group (Managed by Terraform)", + "egress": [ + { + "cidr_blocks": [ + "0.0.0.0/0" + ], + "description": "required", + "from_port": 0, + "ipv6_cidr_blocks": [ + ], + "prefix_list_ids": [ + ], + "protocol": "-1", + "security_groups": [ + ], + "self": null, + "to_port": 0 + } + ], + "ingress": [ + { + "cidr_blocks": [ + ], + "description": "required", + "from_port": 80, + "ipv6_cidr_blocks": [ + ], + "prefix_list_ids": [ + ], + "protocol": "TCP", + "security_groups": [ + "\${aws_security_group.testPocketApp_application_load_balancer_alb_security_group_91A27046.id}" + ], + "self": null, + "to_port": 8675309 + } + ], + "lifecycle": { + "create_before_destroy": true + }, + "name_prefix": "testapp-ECSSecurityGroup", + "vpc_id": "\${data.aws_vpc.testPocketApp_pocket_vpc_C4E157E3.id}" + } + } + } +}" +`; + +exports[`PocketALBApplication renders an Pocket App with logs and dashboard in a specified region 1`] = ` +"{ + "data": { + "aws_caller_identity": { + "testPocketApp_pocket_vpc_current_identity_06D87057": { + } + }, + "aws_iam_policy_document": { + "testPocketApp_ecs_service_ecs-iam_ecs-task-assume_ABB81680": { + "statement": [ + { + "actions": [ + "sts:AssumeRole" + ], + "effect": "Allow", + "principals": [ + { + "identifiers": [ + "ecs-tasks.amazonaws.com" + ], + "type": "Service" + } + ] + } + ], + "version": "2012-10-17" + } + }, + "aws_kms_alias": { + "testPocketApp_pocket_vpc_secrets_manager_key_9FD2BC93": { + "name": "alias/aws/secretsmanager" + } + }, + "aws_region": { + "testPocketApp_pocket_vpc_current_region_8ED435E7": { + } + }, + "aws_route53_zone": { + "testPocketApp_base_dns_main_hosted_zone_9442EEB0": { + "name": "bowling.gov" + } + }, + "aws_security_groups": { + "testPocketApp_pocket_vpc_default_security_groups_40B4FC48": { + "filter": [ + { + "name": "group-name", + "values": [ + "default" + ] + }, + { + "name": "vpc-id", + "values": [ + "\${data.aws_vpc.testPocketApp_pocket_vpc_C4E157E3.id}" + ] + } + ] + }, + "testPocketApp_pocket_vpc_internal_security_groups_12F7F666": { + "filter": [ + { + "name": "group-name", + "values": [ + "pocket-vpc-internal" + ] + }, + { + "name": "vpc-id", + "values": [ + "\${data.aws_vpc.testPocketApp_pocket_vpc_C4E157E3.id}" + ] + } + ] + } + }, + "aws_ssm_parameter": { + "testPocketApp_pocket_vpc_private_subnets_9D449563": { + "name": "/Shared/PrivateSubnets" + }, + "testPocketApp_pocket_vpc_public_subnets_B0B3A6AB": { + "name": "/Shared/PublicSubnets" + }, + "testPocketApp_pocket_vpc_vpc_ssm_param_235525D4": { + "name": "/Shared/Vpc" + } + }, + "aws_subnets": { + "testPocketApp_pocket_vpc_private_subnet_ids_EB1E3A65": { + "filter": [ + { + "name": "subnet-id", + "values": "\${split(\\",\\", data.aws_ssm_parameter.testPocketApp_pocket_vpc_private_subnets_9D449563.value)}" + }, + { + "name": "vpc-id", + "values": [ + "\${data.aws_vpc.testPocketApp_pocket_vpc_C4E157E3.id}" + ] + } + ] + }, + "testPocketApp_pocket_vpc_public_subnet_ids_01F0B902": { + "filter": [ + { + "name": "subnet-id", + "values": "\${split(\\",\\", data.aws_ssm_parameter.testPocketApp_pocket_vpc_public_subnets_B0B3A6AB.value)}" + }, + { + "name": "vpc-id", + "values": [ + "\${data.aws_vpc.testPocketApp_pocket_vpc_C4E157E3.id}" + ] + } + ] + } + }, + "aws_vpc": { + "testPocketApp_pocket_vpc_C4E157E3": { + "filter": [ + { + "name": "vpc-id", + "values": [ + "\${data.aws_ssm_parameter.testPocketApp_pocket_vpc_vpc_ssm_param_235525D4.value}" + ] + } + ] + } + } + }, + "resource": { + "aws_acm_certificate": { + "testPocketApp_alb_certificate_417C14FF": { + "domain_name": "testing.bowling.gov", + "lifecycle": { + "create_before_destroy": true + }, + "validation_method": "DNS" + } + }, + "aws_acm_certificate_validation": { + "testPocketApp_alb_certificate_certificate_validation_7D899C6A": { + "certificate_arn": "\${aws_acm_certificate.testPocketApp_alb_certificate_417C14FF.arn}", + "depends_on": [ + "aws_route53_record.testPocketApp_alb_certificate_certificate_record_3C49201F", + "aws_acm_certificate.testPocketApp_alb_certificate_417C14FF" + ], + "validation_record_fqdns": [ + "\${aws_route53_record.testPocketApp_alb_certificate_certificate_record_3C49201F.fqdn}" + ] + } + }, + "aws_alb": { + "testPocketApp_application_load_balancer_alb_D538DEEE": { + "internal": false, + "name_prefix": "TSTAPP", + "security_groups": [ + "\${aws_security_group.testPocketApp_application_load_balancer_alb_security_group_91A27046.id}" + ], + "subnets": "\${data.aws_subnets.testPocketApp_pocket_vpc_public_subnet_ids_01F0B902.ids}" + } + }, + "aws_alb_listener": { + "testPocketApp_listener_http_A9E227C2": { + "default_action": [ + { + "redirect": { + "port": "443", + "protocol": "HTTPS", + "status_code": "HTTP_301" + }, + "type": "redirect" + } + ], + "load_balancer_arn": "\${aws_alb.testPocketApp_application_load_balancer_alb_D538DEEE.arn}", + "port": 80, + "protocol": "HTTP" + }, + "testPocketApp_listener_https_7F49DE83": { + "certificate_arn": "\${aws_acm_certificate.testPocketApp_alb_certificate_417C14FF.arn}", + "default_action": [ + { + "fixed_response": { + "content_type": "text/plain", + "message_body": "", + "status_code": "503" + }, + "type": "fixed-response" + } + ], + "load_balancer_arn": "\${aws_alb.testPocketApp_application_load_balancer_alb_D538DEEE.arn}", + "port": 443, + "protocol": "HTTPS", + "ssl_policy": "ELBSecurityPolicy-TLS-1-1-2017-01" + } + }, + "aws_alb_listener_rule": { + "testPocketApp_ecs_service_listener_rule_C03BB04D": { + "action": [ + { + "target_group_arn": "\${aws_alb_target_group.testPocketApp_ecs_service_blue_target_group_ecs_target_group_A4B85885.arn}", + "type": "forward" + } + ], + "condition": [ + { + "path_pattern": { + "values": [ + "*" + ] + } + } + ], + "lifecycle": { + "ignore_changes": [ + "action" + ] + }, + "listener_arn": "\${aws_alb_listener.testPocketApp_listener_https_7F49DE83.arn}", + "priority": 1 + } + }, + "aws_alb_target_group": { + "testPocketApp_ecs_service_blue_target_group_ecs_target_group_A4B85885": { + "deregistration_delay": "120", + "health_check": { + "healthy_threshold": 5, + "interval": 15, + "path": "/test", + "protocol": "HTTP", + "unhealthy_threshold": 3 + }, + "name_prefix": "TSTAPP", + "port": 80, + "protocol": "HTTP", + "tags": { + "type": "blue" + }, + "target_type": "ip", + "vpc_id": "\${data.aws_vpc.testPocketApp_pocket_vpc_C4E157E3.id}" + } + }, + "aws_appautoscaling_policy": { + "testPocketApp_autoscaling_scale_in_policy_A163A235": { + "depends_on": [ + "aws_appautoscaling_target.testPocketApp_autoscaling_autoscaling_target_505E4EA1" + ], + "name": "testapp-ScaleInPolicy", + "policy_type": "StepScaling", + "resource_id": "\${aws_appautoscaling_target.testPocketApp_autoscaling_autoscaling_target_505E4EA1.resource_id}", + "scalable_dimension": "\${aws_appautoscaling_target.testPocketApp_autoscaling_autoscaling_target_505E4EA1.scalable_dimension}", + "service_namespace": "\${aws_appautoscaling_target.testPocketApp_autoscaling_autoscaling_target_505E4EA1.service_namespace}", + "step_scaling_policy_configuration": { + "adjustment_type": "ChangeInCapacity", + "cooldown": 60, + "metric_aggregation_type": "Average", + "step_adjustment": [ + { + "metric_interval_upper_bound": "0", + "scaling_adjustment": -1 + } + ] + }, + "target_tracking_scaling_policy_configuration": null + }, + "testPocketApp_autoscaling_scale_out_policy_075BFAC4": { + "depends_on": [ + "aws_appautoscaling_target.testPocketApp_autoscaling_autoscaling_target_505E4EA1" + ], + "name": "testapp-ScaleOutPolicy", + "policy_type": "StepScaling", + "resource_id": "\${aws_appautoscaling_target.testPocketApp_autoscaling_autoscaling_target_505E4EA1.resource_id}", + "scalable_dimension": "\${aws_appautoscaling_target.testPocketApp_autoscaling_autoscaling_target_505E4EA1.scalable_dimension}", + "service_namespace": "\${aws_appautoscaling_target.testPocketApp_autoscaling_autoscaling_target_505E4EA1.service_namespace}", + "step_scaling_policy_configuration": { + "adjustment_type": "ChangeInCapacity", + "cooldown": 60, + "metric_aggregation_type": "Average", + "step_adjustment": [ + { + "metric_interval_lower_bound": "0", + "scaling_adjustment": 2 + } + ] + }, + "target_tracking_scaling_policy_configuration": null + } + }, + "aws_appautoscaling_target": { + "testPocketApp_autoscaling_autoscaling_target_505E4EA1": { + "max_capacity": 2, + "min_capacity": 1, + "resource_id": "service/\${aws_ecs_cluster.testPocketApp_ecs_cluster_C3960066.name}/\${aws_ecs_service.testPocketApp_ecs_service_ecs-service_182DEA4C.name}", + "scalable_dimension": "ecs:service:DesiredCount", + "service_namespace": "ecs" + } + }, + "aws_cloudwatch_dashboard": { + "testPocketApp_cloudwatch-dashboard_26E9F70C": { + "dashboard_body": "{\\"widgets\\":[{\\"type\\":\\"metric\\",\\"x\\":0,\\"y\\":0,\\"width\\":12,\\"height\\":6,\\"properties\\":{\\"metrics\\":[[\\"AWS/ApplicationELB\\",\\"HTTPCode_Target_4XX_Count\\",\\"LoadBalancer\\",\\"\${aws_alb.testPocketApp_application_load_balancer_alb_D538DEEE.arn_suffix}\\",{\\"yAxis\\":\\"left\\",\\"color\\":\\"#ff7f0e\\"}],[\\".\\",\\"RequestCount\\",\\".\\",\\".\\",{\\"yAxis\\":\\"right\\",\\"color\\":\\"#1f77b4\\"}],[\\".\\",\\"HTTPCode_Target_5XX_Count\\",\\".\\",\\".\\",{\\"color\\":\\"#d62728\\"}],[\\".\\",\\"HTTPCode_Target_2XX_Count\\",\\".\\",\\".\\",{\\"yAxis\\":\\"right\\",\\"color\\":\\"#2ca02c\\"}]],\\"view\\":\\"timeSeries\\",\\"stacked\\":false,\\"region\\":\\"central region\\",\\"period\\":60,\\"stat\\":\\"Sum\\",\\"title\\":\\"Target Requests\\"}},{\\"type\\":\\"metric\\",\\"x\\":12,\\"y\\":0,\\"width\\":12,\\"height\\":6,\\"properties\\":{\\"metrics\\":[[\\"AWS/ApplicationELB\\",\\"HTTPCode_ELB_4XX_Count\\",\\"LoadBalancer\\",\\"\${aws_alb.testPocketApp_application_load_balancer_alb_D538DEEE.arn_suffix}\\",{\\"yAxis\\":\\"left\\",\\"color\\":\\"#ff7f0e\\"}],[\\".\\",\\"RequestCount\\",\\".\\",\\".\\",{\\"yAxis\\":\\"right\\",\\"color\\":\\"#1f77b4\\"}],[\\".\\",\\"HTTPCode_ELB_5XX_Count\\",\\".\\",\\".\\",{\\"color\\":\\"#d62728\\"}]],\\"view\\":\\"timeSeries\\",\\"stacked\\":false,\\"region\\":\\"central region\\",\\"period\\":60,\\"stat\\":\\"Sum\\",\\"title\\":\\"ALB Requests\\"}},{\\"type\\":\\"metric\\",\\"x\\":12,\\"y\\":6,\\"width\\":12,\\"height\\":6,\\"properties\\":{\\"metrics\\":[[\\"AWS/ApplicationELB\\",\\"TargetResponseTime\\",\\"LoadBalancer\\",\\"\${aws_alb.testPocketApp_application_load_balancer_alb_D538DEEE.arn_suffix}\\",{\\"label\\":\\"Average\\",\\"color\\":\\"#aec7e8\\"}],[\\"...\\",{\\"stat\\":\\"p95\\",\\"label\\":\\"p95\\",\\"color\\":\\"#ffbb78\\"}],[\\"...\\",{\\"stat\\":\\"p99\\",\\"label\\":\\"p99\\",\\"color\\":\\"#98df8a\\"}]],\\"view\\":\\"timeSeries\\",\\"stacked\\":false,\\"region\\":\\"central region\\",\\"stat\\":\\"Average\\",\\"period\\":60}},{\\"type\\":\\"metric\\",\\"x\\":0,\\"y\\":6,\\"width\\":12,\\"height\\":6,\\"properties\\":{\\"metrics\\":[[\\"ECS/ContainerInsights\\",\\"RunningTaskCount\\",\\"ServiceName\\",\\"\${aws_ecs_service.testPocketApp_ecs_service_ecs-service_182DEA4C.name}\\",\\"ClusterName\\",\\"\${aws_ecs_cluster.testPocketApp_ecs_cluster_C3960066.name}\\",{\\"yAxis\\":\\"right\\",\\"color\\":\\"#c49c94\\"}],[\\"AWS/ECS\\",\\"CPUUtilization\\",\\".\\",\\".\\",\\".\\",\\".\\",{\\"color\\":\\"#f7b6d2\\"}],[\\".\\",\\"MemoryUtilization\\",\\".\\",\\".\\",\\".\\",\\".\\",{\\"color\\":\\"#c7c7c7\\"}]],\\"view\\":\\"timeSeries\\",\\"stacked\\":false,\\"region\\":\\"central region\\",\\"stat\\":\\"Average\\",\\"period\\":60,\\"annotations\\":{\\"horizontal\\":[{\\"color\\":\\"#e377c2\\",\\"label\\":\\"CPU scale out\\",\\"value\\":45},{\\"color\\":\\"#c5b0d5\\",\\"label\\":\\"CPU scale in\\",\\"value\\":30}]},\\"title\\":\\"Service Load\\"}}]}", + "dashboard_name": "testapp", + "lifecycle": { + "ignore_changes": [ + "dashboard_body" + ] + } + } + }, + "aws_cloudwatch_metric_alarm": { + "testPocketApp_autoscaling_scale_in_alarm_69B5F00D": { + "alarm_actions": [ + "\${aws_appautoscaling_policy.testPocketApp_autoscaling_scale_in_policy_A163A235.arn}" + ], + "alarm_description": "Alarm to reduce capacity if container CPU is low", + "alarm_name": "testapp Service Low CPU", + "comparison_operator": "LessThanThreshold", + "dimensions": { + "ClusterName": "\${aws_ecs_cluster.testPocketApp_ecs_cluster_C3960066.name}", + "ServiceName": "\${aws_ecs_service.testPocketApp_ecs_service_ecs-service_182DEA4C.name}" + }, + "evaluation_periods": 2, + "metric_name": "CPUUtilization", + "namespace": "AWS/ECS", + "period": 60, + "statistic": "Average", + "threshold": 30, + "treat_missing_data": "notBreaching" + }, + "testPocketApp_autoscaling_scale_out_alarm_4313FBE9": { + "alarm_actions": [ + "\${aws_appautoscaling_policy.testPocketApp_autoscaling_scale_out_policy_075BFAC4.arn}" + ], + "alarm_description": "Alarm to add capacity if container CPU is high", + "alarm_name": "testapp Service High CPU", + "comparison_operator": "GreaterThanThreshold", + "dimensions": { + "ClusterName": "\${aws_ecs_cluster.testPocketApp_ecs_cluster_C3960066.name}", + "ServiceName": "\${aws_ecs_service.testPocketApp_ecs_service_ecs-service_182DEA4C.name}" + }, + "evaluation_periods": 2, + "metric_name": "CPUUtilization", + "namespace": "AWS/ECS", + "period": 60, + "statistic": "Average", + "threshold": 45, + "treat_missing_data": "notBreaching" + } + }, + "aws_ecs_cluster": { + "testPocketApp_ecs_cluster_C3960066": { + "name": "testapp", + "setting": [ + { + "name": "containerInsights", + "value": "enabled" + } + ] + } + }, + "aws_ecs_service": { + "testPocketApp_ecs_service_ecs-service_182DEA4C": { + "cluster": "\${aws_ecs_cluster.testPocketApp_ecs_cluster_C3960066.arn}", + "depends_on": [ + "aws_alb_target_group.testPocketApp_ecs_service_blue_target_group_ecs_target_group_A4B85885", + "aws_alb_listener_rule.testPocketApp_ecs_service_listener_rule_C03BB04D" + ], + "deployment_controller": { + "type": "ECS" + }, + "deployment_maximum_percent": 200, + "deployment_minimum_healthy_percent": 100, + "desired_count": 2, + "launch_type": "FARGATE", + "lifecycle": { + "ignore_changes": [ + "desired_count", + "load_balancer" + ] + }, + "load_balancer": [ + { + "container_name": "main_container", + "container_port": 8675309, + "target_group_arn": "\${aws_alb_target_group.testPocketApp_ecs_service_blue_target_group_ecs_target_group_A4B85885.arn}" + } + ], + "name": "testapp", + "network_configuration": { + "security_groups": [ + "\${aws_security_group.testPocketApp_ecs_service_ecs_security_group_9C016FA8.id}" + ], + "subnets": "\${data.aws_subnets.testPocketApp_pocket_vpc_private_subnet_ids_EB1E3A65.ids}" + }, + "propagate_tags": "SERVICE", + "task_definition": "\${aws_ecs_task_definition.testPocketApp_ecs_service_ecs-task_A7E74E45.arn}" + } + }, + "aws_ecs_task_definition": { + "testPocketApp_ecs_service_ecs-task_A7E74E45": { + "container_definitions": "[]", + "cpu": "512", + "depends_on": [ + ], + "execution_role_arn": "\${aws_iam_role.testPocketApp_ecs_service_ecs-iam_ecs-execution-role_EB461A0C.arn}", + "family": "testapp", + "memory": "2048", + "network_mode": "awsvpc", + "requires_compatibilities": [ + "FARGATE" + ], + "task_role_arn": "\${aws_iam_role.testPocketApp_ecs_service_ecs-iam_ecs-task-role_7DB93963.arn}", + "volume": [ + ] + } + }, + "aws_iam_role": { + "testPocketApp_ecs_service_ecs-iam_ecs-execution-role_EB461A0C": { + "assume_role_policy": "\${data.aws_iam_policy_document.testPocketApp_ecs_service_ecs-iam_ecs-task-assume_ABB81680.json}", + "name": "testapp-TaskExecutionRole" + }, + "testPocketApp_ecs_service_ecs-iam_ecs-task-role_7DB93963": { + "assume_role_policy": "\${data.aws_iam_policy_document.testPocketApp_ecs_service_ecs-iam_ecs-task-assume_ABB81680.json}", + "name": "testapp-TaskRole" + } + }, + "aws_route53_record": { + "testPocketApp_alb_certificate_certificate_record_3C49201F": { + "depends_on": [ + "aws_acm_certificate.testPocketApp_alb_certificate_417C14FF" + ], + "name": "\${tolist(aws_acm_certificate.testPocketApp_alb_certificate_417C14FF.domain_validation_options).0.resource_record_name}", + "records": [ + "\${tolist(aws_acm_certificate.testPocketApp_alb_certificate_417C14FF.domain_validation_options).0.resource_record_value}" + ], + "ttl": 60, + "type": "\${tolist(aws_acm_certificate.testPocketApp_alb_certificate_417C14FF.domain_validation_options).0.resource_record_type}", + "zone_id": "\${aws_route53_zone.testPocketApp_base_dns_subhosted_zone_1E1E3243.zone_id}" + }, + "testPocketApp_alb_record_7B56ED33": { + "alias": { + "evaluate_target_health": true, + "name": "\${aws_alb.testPocketApp_application_load_balancer_alb_D538DEEE.dns_name}", + "zone_id": "\${aws_alb.testPocketApp_application_load_balancer_alb_D538DEEE.zone_id}" + }, + "lifecycle": { + "ignore_changes": [ + "weighted_routing_policy[0].weight" + ] + }, + "name": "testing.bowling.gov", + "set_identifier": "1", + "type": "A", + "weighted_routing_policy": { + "weight": 1 + }, + "zone_id": "\${aws_route53_zone.testPocketApp_base_dns_subhosted_zone_1E1E3243.zone_id}" + }, + "testPocketApp_base_dns_subhosted_zone_ns_497FFB83": { + "name": "testing.bowling.gov", + "records": "\${aws_route53_zone.testPocketApp_base_dns_subhosted_zone_1E1E3243.name_servers}", + "ttl": 86400, + "type": "NS", + "zone_id": "\${data.aws_route53_zone.testPocketApp_base_dns_main_hosted_zone_9442EEB0.zone_id}" + } + }, + "aws_route53_zone": { + "testPocketApp_base_dns_subhosted_zone_1E1E3243": { + "name": "testing.bowling.gov" + } + }, + "aws_security_group": { + "testPocketApp_application_load_balancer_alb_security_group_91A27046": { + "description": "External security group (Managed by Terraform)", + "egress": [ + { + "cidr_blocks": [ + "0.0.0.0/0" + ], + "description": "required", + "from_port": 0, + "ipv6_cidr_blocks": [ + ], + "prefix_list_ids": [ + ], + "protocol": "-1", + "security_groups": [ + ], + "self": null, + "to_port": 0 + } + ], + "ingress": [ + { + "cidr_blocks": [ + "0.0.0.0/0" + ], + "description": null, + "from_port": 443, + "ipv6_cidr_blocks": null, + "prefix_list_ids": null, + "protocol": "TCP", + "security_groups": null, + "self": null, + "to_port": 443 + }, + { + "cidr_blocks": [ + "0.0.0.0/0" + ], + "description": null, + "from_port": 80, + "ipv6_cidr_blocks": null, + "prefix_list_ids": null, + "protocol": "TCP", + "security_groups": null, + "self": null, + "to_port": 80 + } + ], + "lifecycle": { + "create_before_destroy": true + }, + "name_prefix": "testapp-HTTP/S Security Group", + "tags": { + "Name": "testapp-HTTP/S Security Group" + }, + "vpc_id": "\${data.aws_vpc.testPocketApp_pocket_vpc_C4E157E3.id}" + }, + "testPocketApp_ecs_service_ecs_security_group_9C016FA8": { + "description": "Internal ECS Security Group (Managed by Terraform)", + "egress": [ + { + "cidr_blocks": [ + "0.0.0.0/0" + ], + "description": "required", + "from_port": 0, + "ipv6_cidr_blocks": [ + ], + "prefix_list_ids": [ + ], + "protocol": "-1", + "security_groups": [ + ], + "self": null, + "to_port": 0 + } + ], + "ingress": [ + { + "cidr_blocks": [ + ], + "description": "required", + "from_port": 80, + "ipv6_cidr_blocks": [ + ], + "prefix_list_ids": [ + ], + "protocol": "TCP", + "security_groups": [ + "\${aws_security_group.testPocketApp_application_load_balancer_alb_security_group_91A27046.id}" + ], + "self": null, + "to_port": 8675309 + } + ], + "lifecycle": { + "create_before_destroy": true + }, + "name_prefix": "testapp-ECSSecurityGroup", + "vpc_id": "\${data.aws_vpc.testPocketApp_pocket_vpc_C4E157E3.id}" + } + } + } +}" +`; + +exports[`PocketALBApplication renders an application alarms 1`] = ` +"{ + "data": { + "aws_caller_identity": { + "testPocketApp_pocket_vpc_current_identity_06D87057": { + } + }, + "aws_iam_policy_document": { + "testPocketApp_ecs_service_ecs-iam_ecs-task-assume_ABB81680": { + "statement": [ + { + "actions": [ + "sts:AssumeRole" + ], + "effect": "Allow", + "principals": [ + { + "identifiers": [ + "ecs-tasks.amazonaws.com" + ], + "type": "Service" + } + ] + } + ], + "version": "2012-10-17" + } + }, + "aws_kms_alias": { + "testPocketApp_pocket_vpc_secrets_manager_key_9FD2BC93": { + "name": "alias/aws/secretsmanager" + } + }, + "aws_region": { + "testPocketApp_pocket_vpc_current_region_8ED435E7": { + } + }, + "aws_route53_zone": { + "testPocketApp_base_dns_main_hosted_zone_9442EEB0": { + "name": "bowling.gov" + } + }, + "aws_security_groups": { + "testPocketApp_pocket_vpc_default_security_groups_40B4FC48": { + "filter": [ + { + "name": "group-name", + "values": [ + "default" + ] + }, + { + "name": "vpc-id", + "values": [ + "\${data.aws_vpc.testPocketApp_pocket_vpc_C4E157E3.id}" + ] + } + ] + }, + "testPocketApp_pocket_vpc_internal_security_groups_12F7F666": { + "filter": [ + { + "name": "group-name", + "values": [ + "pocket-vpc-internal" + ] + }, + { + "name": "vpc-id", + "values": [ + "\${data.aws_vpc.testPocketApp_pocket_vpc_C4E157E3.id}" + ] + } + ] + } + }, + "aws_ssm_parameter": { + "testPocketApp_pocket_vpc_private_subnets_9D449563": { + "name": "/Shared/PrivateSubnets" + }, + "testPocketApp_pocket_vpc_public_subnets_B0B3A6AB": { + "name": "/Shared/PublicSubnets" + }, + "testPocketApp_pocket_vpc_vpc_ssm_param_235525D4": { + "name": "/Shared/Vpc" + } + }, + "aws_subnets": { + "testPocketApp_pocket_vpc_private_subnet_ids_EB1E3A65": { + "filter": [ + { + "name": "subnet-id", + "values": "\${split(\\",\\", data.aws_ssm_parameter.testPocketApp_pocket_vpc_private_subnets_9D449563.value)}" + }, + { + "name": "vpc-id", + "values": [ + "\${data.aws_vpc.testPocketApp_pocket_vpc_C4E157E3.id}" + ] + } + ] + }, + "testPocketApp_pocket_vpc_public_subnet_ids_01F0B902": { + "filter": [ + { + "name": "subnet-id", + "values": "\${split(\\",\\", data.aws_ssm_parameter.testPocketApp_pocket_vpc_public_subnets_B0B3A6AB.value)}" + }, + { + "name": "vpc-id", + "values": [ + "\${data.aws_vpc.testPocketApp_pocket_vpc_C4E157E3.id}" + ] + } + ] + } + }, + "aws_vpc": { + "testPocketApp_pocket_vpc_C4E157E3": { + "filter": [ + { + "name": "vpc-id", + "values": [ + "\${data.aws_ssm_parameter.testPocketApp_pocket_vpc_vpc_ssm_param_235525D4.value}" + ] + } + ] + } + } + }, + "resource": { + "aws_acm_certificate": { + "testPocketApp_alb_certificate_417C14FF": { + "domain_name": "testing.bowling.gov", + "lifecycle": { + "create_before_destroy": true + }, + "validation_method": "DNS" + } + }, + "aws_acm_certificate_validation": { + "testPocketApp_alb_certificate_certificate_validation_7D899C6A": { + "certificate_arn": "\${aws_acm_certificate.testPocketApp_alb_certificate_417C14FF.arn}", + "depends_on": [ + "aws_route53_record.testPocketApp_alb_certificate_certificate_record_3C49201F", + "aws_acm_certificate.testPocketApp_alb_certificate_417C14FF" + ], + "validation_record_fqdns": [ + "\${aws_route53_record.testPocketApp_alb_certificate_certificate_record_3C49201F.fqdn}" + ] + } + }, + "aws_alb": { + "testPocketApp_application_load_balancer_alb_D538DEEE": { + "internal": false, + "name_prefix": "TSTAPP", + "security_groups": [ + "\${aws_security_group.testPocketApp_application_load_balancer_alb_security_group_91A27046.id}" + ], + "subnets": "\${data.aws_subnets.testPocketApp_pocket_vpc_public_subnet_ids_01F0B902.ids}" + } + }, + "aws_alb_listener": { + "testPocketApp_listener_http_A9E227C2": { + "default_action": [ + { + "redirect": { + "port": "443", + "protocol": "HTTPS", + "status_code": "HTTP_301" + }, + "type": "redirect" + } + ], + "load_balancer_arn": "\${aws_alb.testPocketApp_application_load_balancer_alb_D538DEEE.arn}", + "port": 80, + "protocol": "HTTP" + }, + "testPocketApp_listener_https_7F49DE83": { + "certificate_arn": "\${aws_acm_certificate.testPocketApp_alb_certificate_417C14FF.arn}", + "default_action": [ + { + "fixed_response": { + "content_type": "text/plain", + "message_body": "", + "status_code": "503" + }, + "type": "fixed-response" + } + ], + "load_balancer_arn": "\${aws_alb.testPocketApp_application_load_balancer_alb_D538DEEE.arn}", + "port": 443, + "protocol": "HTTPS", + "ssl_policy": "ELBSecurityPolicy-TLS-1-1-2017-01" + } + }, + "aws_alb_listener_rule": { + "testPocketApp_ecs_service_listener_rule_C03BB04D": { + "action": [ + { + "target_group_arn": "\${aws_alb_target_group.testPocketApp_ecs_service_blue_target_group_ecs_target_group_A4B85885.arn}", + "type": "forward" + } + ], + "condition": [ + { + "path_pattern": { + "values": [ + "*" + ] + } + } + ], + "lifecycle": { + "ignore_changes": [ + "action" + ] + }, + "listener_arn": "\${aws_alb_listener.testPocketApp_listener_https_7F49DE83.arn}", + "priority": 1 + } + }, + "aws_alb_target_group": { + "testPocketApp_ecs_service_blue_target_group_ecs_target_group_A4B85885": { + "deregistration_delay": "120", + "health_check": { + "healthy_threshold": 5, + "interval": 15, + "path": "/test", + "protocol": "HTTP", + "unhealthy_threshold": 3 + }, + "name_prefix": "TSTAPP", + "port": 80, + "protocol": "HTTP", + "tags": { + "type": "blue" + }, + "target_type": "ip", + "vpc_id": "\${data.aws_vpc.testPocketApp_pocket_vpc_C4E157E3.id}" + } + }, + "aws_appautoscaling_policy": { + "testPocketApp_autoscaling_scale_in_policy_A163A235": { + "depends_on": [ + "aws_appautoscaling_target.testPocketApp_autoscaling_autoscaling_target_505E4EA1" + ], + "name": "testapp-ScaleInPolicy", + "policy_type": "StepScaling", + "resource_id": "\${aws_appautoscaling_target.testPocketApp_autoscaling_autoscaling_target_505E4EA1.resource_id}", + "scalable_dimension": "\${aws_appautoscaling_target.testPocketApp_autoscaling_autoscaling_target_505E4EA1.scalable_dimension}", + "service_namespace": "\${aws_appautoscaling_target.testPocketApp_autoscaling_autoscaling_target_505E4EA1.service_namespace}", + "step_scaling_policy_configuration": { + "adjustment_type": "ChangeInCapacity", + "cooldown": 60, + "metric_aggregation_type": "Average", + "step_adjustment": [ + { + "metric_interval_upper_bound": "0", + "scaling_adjustment": -1 + } + ] + }, + "target_tracking_scaling_policy_configuration": null + }, + "testPocketApp_autoscaling_scale_out_policy_075BFAC4": { + "depends_on": [ + "aws_appautoscaling_target.testPocketApp_autoscaling_autoscaling_target_505E4EA1" + ], + "name": "testapp-ScaleOutPolicy", + "policy_type": "StepScaling", + "resource_id": "\${aws_appautoscaling_target.testPocketApp_autoscaling_autoscaling_target_505E4EA1.resource_id}", + "scalable_dimension": "\${aws_appautoscaling_target.testPocketApp_autoscaling_autoscaling_target_505E4EA1.scalable_dimension}", + "service_namespace": "\${aws_appautoscaling_target.testPocketApp_autoscaling_autoscaling_target_505E4EA1.service_namespace}", + "step_scaling_policy_configuration": { + "adjustment_type": "ChangeInCapacity", + "cooldown": 60, + "metric_aggregation_type": "Average", + "step_adjustment": [ + { + "metric_interval_lower_bound": "0", + "scaling_adjustment": 2 + } + ] + }, + "target_tracking_scaling_policy_configuration": null + } + }, + "aws_appautoscaling_target": { + "testPocketApp_autoscaling_autoscaling_target_505E4EA1": { + "max_capacity": 2, + "min_capacity": 1, + "resource_id": "service/\${aws_ecs_cluster.testPocketApp_ecs_cluster_C3960066.name}/\${aws_ecs_service.testPocketApp_ecs_service_ecs-service_182DEA4C.name}", + "scalable_dimension": "ecs:service:DesiredCount", + "service_namespace": "ecs" + } + }, + "aws_cloudwatch_dashboard": { + "testPocketApp_cloudwatch-dashboard_26E9F70C": { + "dashboard_body": "{\\"widgets\\":[{\\"type\\":\\"metric\\",\\"x\\":0,\\"y\\":0,\\"width\\":12,\\"height\\":6,\\"properties\\":{\\"metrics\\":[[\\"AWS/ApplicationELB\\",\\"HTTPCode_Target_4XX_Count\\",\\"LoadBalancer\\",\\"\${aws_alb.testPocketApp_application_load_balancer_alb_D538DEEE.arn_suffix}\\",{\\"yAxis\\":\\"left\\",\\"color\\":\\"#ff7f0e\\"}],[\\".\\",\\"RequestCount\\",\\".\\",\\".\\",{\\"yAxis\\":\\"right\\",\\"color\\":\\"#1f77b4\\"}],[\\".\\",\\"HTTPCode_Target_5XX_Count\\",\\".\\",\\".\\",{\\"color\\":\\"#d62728\\"}],[\\".\\",\\"HTTPCode_Target_2XX_Count\\",\\".\\",\\".\\",{\\"yAxis\\":\\"right\\",\\"color\\":\\"#2ca02c\\"}]],\\"view\\":\\"timeSeries\\",\\"stacked\\":false,\\"region\\":\\"us-east-1\\",\\"period\\":60,\\"stat\\":\\"Sum\\",\\"title\\":\\"Target Requests\\"}},{\\"type\\":\\"metric\\",\\"x\\":12,\\"y\\":0,\\"width\\":12,\\"height\\":6,\\"properties\\":{\\"metrics\\":[[\\"AWS/ApplicationELB\\",\\"HTTPCode_ELB_4XX_Count\\",\\"LoadBalancer\\",\\"\${aws_alb.testPocketApp_application_load_balancer_alb_D538DEEE.arn_suffix}\\",{\\"yAxis\\":\\"left\\",\\"color\\":\\"#ff7f0e\\"}],[\\".\\",\\"RequestCount\\",\\".\\",\\".\\",{\\"yAxis\\":\\"right\\",\\"color\\":\\"#1f77b4\\"}],[\\".\\",\\"HTTPCode_ELB_5XX_Count\\",\\".\\",\\".\\",{\\"color\\":\\"#d62728\\"}]],\\"view\\":\\"timeSeries\\",\\"stacked\\":false,\\"region\\":\\"us-east-1\\",\\"period\\":60,\\"stat\\":\\"Sum\\",\\"title\\":\\"ALB Requests\\"}},{\\"type\\":\\"metric\\",\\"x\\":12,\\"y\\":6,\\"width\\":12,\\"height\\":6,\\"properties\\":{\\"metrics\\":[[\\"AWS/ApplicationELB\\",\\"TargetResponseTime\\",\\"LoadBalancer\\",\\"\${aws_alb.testPocketApp_application_load_balancer_alb_D538DEEE.arn_suffix}\\",{\\"label\\":\\"Average\\",\\"color\\":\\"#aec7e8\\"}],[\\"...\\",{\\"stat\\":\\"p95\\",\\"label\\":\\"p95\\",\\"color\\":\\"#ffbb78\\"}],[\\"...\\",{\\"stat\\":\\"p99\\",\\"label\\":\\"p99\\",\\"color\\":\\"#98df8a\\"}]],\\"view\\":\\"timeSeries\\",\\"stacked\\":false,\\"region\\":\\"us-east-1\\",\\"stat\\":\\"Average\\",\\"period\\":60}},{\\"type\\":\\"metric\\",\\"x\\":0,\\"y\\":6,\\"width\\":12,\\"height\\":6,\\"properties\\":{\\"metrics\\":[[\\"ECS/ContainerInsights\\",\\"RunningTaskCount\\",\\"ServiceName\\",\\"\${aws_ecs_service.testPocketApp_ecs_service_ecs-service_182DEA4C.name}\\",\\"ClusterName\\",\\"\${aws_ecs_cluster.testPocketApp_ecs_cluster_C3960066.name}\\",{\\"yAxis\\":\\"right\\",\\"color\\":\\"#c49c94\\"}],[\\"AWS/ECS\\",\\"CPUUtilization\\",\\".\\",\\".\\",\\".\\",\\".\\",{\\"color\\":\\"#f7b6d2\\"}],[\\".\\",\\"MemoryUtilization\\",\\".\\",\\".\\",\\".\\",\\".\\",{\\"color\\":\\"#c7c7c7\\"}]],\\"view\\":\\"timeSeries\\",\\"stacked\\":false,\\"region\\":\\"us-east-1\\",\\"stat\\":\\"Average\\",\\"period\\":60,\\"annotations\\":{\\"horizontal\\":[{\\"color\\":\\"#e377c2\\",\\"label\\":\\"CPU scale out\\",\\"value\\":45},{\\"color\\":\\"#c5b0d5\\",\\"label\\":\\"CPU scale in\\",\\"value\\":30}]},\\"title\\":\\"Service Load\\"}}]}", + "dashboard_name": "testapp", + "lifecycle": { + "ignore_changes": [ + "dashboard_body" + ] + } + } + }, + "aws_cloudwatch_metric_alarm": { + "testPocketApp_alarm-httpresponsetime_327A4315": { + "alarm_actions": [ + "sns-arn-for-latency" + ], + "alarm_description": "Average HTTP response time exceeds threshold", + "alarm_name": "testapp-Alarm-HTTPResponseTime", + "comparison_operator": "GreaterThanThreshold", + "datapoints_to_alarm": 2, + "dimensions": { + "LoadBalancer": "\${aws_alb.testPocketApp_application_load_balancer_alb_D538DEEE.arn_suffix}" + }, + "evaluation_periods": 2, + "insufficient_data_actions": [ + ], + "metric_name": "TargetResponseTime", + "namespace": "AWS/ApplicationELB", + "ok_actions": [ + "sns-arn-for-latency" + ], + "period": 300, + "statistic": "Average", + "threshold": 0.5 + }, + "testPocketApp_autoscaling_scale_in_alarm_69B5F00D": { + "alarm_actions": [ + "\${aws_appautoscaling_policy.testPocketApp_autoscaling_scale_in_policy_A163A235.arn}" + ], + "alarm_description": "Alarm to reduce capacity if container CPU is low", + "alarm_name": "testapp Service Low CPU", + "comparison_operator": "LessThanThreshold", + "dimensions": { + "ClusterName": "\${aws_ecs_cluster.testPocketApp_ecs_cluster_C3960066.name}", + "ServiceName": "\${aws_ecs_service.testPocketApp_ecs_service_ecs-service_182DEA4C.name}" + }, + "evaluation_periods": 2, + "metric_name": "CPUUtilization", + "namespace": "AWS/ECS", + "period": 60, + "statistic": "Average", + "threshold": 30, + "treat_missing_data": "notBreaching" + }, + "testPocketApp_autoscaling_scale_out_alarm_4313FBE9": { + "alarm_actions": [ + "\${aws_appautoscaling_policy.testPocketApp_autoscaling_scale_out_policy_075BFAC4.arn}" + ], + "alarm_description": "Alarm to add capacity if container CPU is high", + "alarm_name": "testapp Service High CPU", + "comparison_operator": "GreaterThanThreshold", + "dimensions": { + "ClusterName": "\${aws_ecs_cluster.testPocketApp_ecs_cluster_C3960066.name}", + "ServiceName": "\${aws_ecs_service.testPocketApp_ecs_service_ecs-service_182DEA4C.name}" + }, + "evaluation_periods": 2, + "metric_name": "CPUUtilization", + "namespace": "AWS/ECS", + "period": 60, + "statistic": "Average", + "threshold": 45, + "treat_missing_data": "notBreaching" + } + }, + "aws_ecs_cluster": { + "testPocketApp_ecs_cluster_C3960066": { + "name": "testapp", + "setting": [ + { + "name": "containerInsights", + "value": "enabled" + } + ] + } + }, + "aws_ecs_service": { + "testPocketApp_ecs_service_ecs-service_182DEA4C": { + "cluster": "\${aws_ecs_cluster.testPocketApp_ecs_cluster_C3960066.arn}", + "depends_on": [ + "aws_alb_target_group.testPocketApp_ecs_service_blue_target_group_ecs_target_group_A4B85885", + "aws_alb_listener_rule.testPocketApp_ecs_service_listener_rule_C03BB04D" + ], + "deployment_controller": { + "type": "ECS" + }, + "deployment_maximum_percent": 200, + "deployment_minimum_healthy_percent": 100, + "desired_count": 2, + "launch_type": "FARGATE", + "lifecycle": { + "ignore_changes": [ + "desired_count", + "load_balancer" + ] + }, + "load_balancer": [ + { + "container_name": "main_container", + "container_port": 8675309, + "target_group_arn": "\${aws_alb_target_group.testPocketApp_ecs_service_blue_target_group_ecs_target_group_A4B85885.arn}" + } + ], + "name": "testapp", + "network_configuration": { + "security_groups": [ + "\${aws_security_group.testPocketApp_ecs_service_ecs_security_group_9C016FA8.id}" + ], + "subnets": "\${data.aws_subnets.testPocketApp_pocket_vpc_private_subnet_ids_EB1E3A65.ids}" + }, + "propagate_tags": "SERVICE", + "task_definition": "\${aws_ecs_task_definition.testPocketApp_ecs_service_ecs-task_A7E74E45.arn}" + } + }, + "aws_ecs_task_definition": { + "testPocketApp_ecs_service_ecs-task_A7E74E45": { + "container_definitions": "[]", + "cpu": "512", + "depends_on": [ + ], + "execution_role_arn": "\${aws_iam_role.testPocketApp_ecs_service_ecs-iam_ecs-execution-role_EB461A0C.arn}", + "family": "testapp", + "memory": "2048", + "network_mode": "awsvpc", + "requires_compatibilities": [ + "FARGATE" + ], + "task_role_arn": "\${aws_iam_role.testPocketApp_ecs_service_ecs-iam_ecs-task-role_7DB93963.arn}", + "volume": [ + ] + } + }, + "aws_iam_role": { + "testPocketApp_ecs_service_ecs-iam_ecs-execution-role_EB461A0C": { + "assume_role_policy": "\${data.aws_iam_policy_document.testPocketApp_ecs_service_ecs-iam_ecs-task-assume_ABB81680.json}", + "name": "testapp-TaskExecutionRole" + }, + "testPocketApp_ecs_service_ecs-iam_ecs-task-role_7DB93963": { + "assume_role_policy": "\${data.aws_iam_policy_document.testPocketApp_ecs_service_ecs-iam_ecs-task-assume_ABB81680.json}", + "name": "testapp-TaskRole" + } + }, + "aws_route53_record": { + "testPocketApp_alb_certificate_certificate_record_3C49201F": { + "depends_on": [ + "aws_acm_certificate.testPocketApp_alb_certificate_417C14FF" + ], + "name": "\${tolist(aws_acm_certificate.testPocketApp_alb_certificate_417C14FF.domain_validation_options).0.resource_record_name}", + "records": [ + "\${tolist(aws_acm_certificate.testPocketApp_alb_certificate_417C14FF.domain_validation_options).0.resource_record_value}" + ], + "ttl": 60, + "type": "\${tolist(aws_acm_certificate.testPocketApp_alb_certificate_417C14FF.domain_validation_options).0.resource_record_type}", + "zone_id": "\${aws_route53_zone.testPocketApp_base_dns_subhosted_zone_1E1E3243.zone_id}" + }, + "testPocketApp_alb_record_7B56ED33": { + "alias": { + "evaluate_target_health": true, + "name": "\${aws_alb.testPocketApp_application_load_balancer_alb_D538DEEE.dns_name}", + "zone_id": "\${aws_alb.testPocketApp_application_load_balancer_alb_D538DEEE.zone_id}" + }, + "lifecycle": { + "ignore_changes": [ + "weighted_routing_policy[0].weight" + ] + }, + "name": "testing.bowling.gov", + "set_identifier": "1", + "type": "A", + "weighted_routing_policy": { + "weight": 1 + }, + "zone_id": "\${aws_route53_zone.testPocketApp_base_dns_subhosted_zone_1E1E3243.zone_id}" + }, + "testPocketApp_base_dns_subhosted_zone_ns_497FFB83": { + "name": "testing.bowling.gov", + "records": "\${aws_route53_zone.testPocketApp_base_dns_subhosted_zone_1E1E3243.name_servers}", + "ttl": 86400, + "type": "NS", + "zone_id": "\${data.aws_route53_zone.testPocketApp_base_dns_main_hosted_zone_9442EEB0.zone_id}" + } + }, + "aws_route53_zone": { + "testPocketApp_base_dns_subhosted_zone_1E1E3243": { + "name": "testing.bowling.gov" + } + }, + "aws_security_group": { + "testPocketApp_application_load_balancer_alb_security_group_91A27046": { + "description": "External security group (Managed by Terraform)", + "egress": [ + { + "cidr_blocks": [ + "0.0.0.0/0" + ], + "description": "required", + "from_port": 0, + "ipv6_cidr_blocks": [ + ], + "prefix_list_ids": [ + ], + "protocol": "-1", + "security_groups": [ + ], + "self": null, + "to_port": 0 + } + ], + "ingress": [ + { + "cidr_blocks": [ + "0.0.0.0/0" + ], + "description": null, + "from_port": 443, + "ipv6_cidr_blocks": null, + "prefix_list_ids": null, + "protocol": "TCP", + "security_groups": null, + "self": null, + "to_port": 443 + }, + { + "cidr_blocks": [ + "0.0.0.0/0" + ], + "description": null, + "from_port": 80, + "ipv6_cidr_blocks": null, + "prefix_list_ids": null, + "protocol": "TCP", + "security_groups": null, + "self": null, + "to_port": 80 + } + ], + "lifecycle": { + "create_before_destroy": true + }, + "name_prefix": "testapp-HTTP/S Security Group", + "tags": { + "Name": "testapp-HTTP/S Security Group" + }, + "vpc_id": "\${data.aws_vpc.testPocketApp_pocket_vpc_C4E157E3.id}" + }, + "testPocketApp_ecs_service_ecs_security_group_9C016FA8": { + "description": "Internal ECS Security Group (Managed by Terraform)", + "egress": [ + { + "cidr_blocks": [ + "0.0.0.0/0" + ], + "description": "required", + "from_port": 0, + "ipv6_cidr_blocks": [ + ], + "prefix_list_ids": [ + ], + "protocol": "-1", + "security_groups": [ + ], + "self": null, + "to_port": 0 + } + ], + "ingress": [ + { + "cidr_blocks": [ + ], + "description": "required", + "from_port": 80, + "ipv6_cidr_blocks": [ + ], + "prefix_list_ids": [ + ], + "protocol": "TCP", + "security_groups": [ + "\${aws_security_group.testPocketApp_application_load_balancer_alb_security_group_91A27046.id}" + ], + "self": null, + "to_port": 8675309 + } + ], + "lifecycle": { + "create_before_destroy": true + }, + "name_prefix": "testapp-ECSSecurityGroup", + "vpc_id": "\${data.aws_vpc.testPocketApp_pocket_vpc_C4E157E3.id}" + } + } + } +}" +`; + +exports[`PocketALBApplication renders an application custom default alarms 1`] = ` +"{ + "data": { + "aws_caller_identity": { + "testPocketApp_pocket_vpc_current_identity_06D87057": { + } + }, + "aws_iam_policy_document": { + "testPocketApp_ecs_service_ecs-iam_ecs-task-assume_ABB81680": { + "statement": [ + { + "actions": [ + "sts:AssumeRole" + ], + "effect": "Allow", + "principals": [ + { + "identifiers": [ + "ecs-tasks.amazonaws.com" + ], + "type": "Service" + } + ] + } + ], + "version": "2012-10-17" + } + }, + "aws_kms_alias": { + "testPocketApp_pocket_vpc_secrets_manager_key_9FD2BC93": { + "name": "alias/aws/secretsmanager" + } + }, + "aws_region": { + "testPocketApp_pocket_vpc_current_region_8ED435E7": { + } + }, + "aws_route53_zone": { + "testPocketApp_base_dns_main_hosted_zone_9442EEB0": { + "name": "bowling.gov" + } + }, + "aws_security_groups": { + "testPocketApp_pocket_vpc_default_security_groups_40B4FC48": { + "filter": [ + { + "name": "group-name", + "values": [ + "default" + ] + }, + { + "name": "vpc-id", + "values": [ + "\${data.aws_vpc.testPocketApp_pocket_vpc_C4E157E3.id}" + ] + } + ] + }, + "testPocketApp_pocket_vpc_internal_security_groups_12F7F666": { + "filter": [ + { + "name": "group-name", + "values": [ + "pocket-vpc-internal" + ] + }, + { + "name": "vpc-id", + "values": [ + "\${data.aws_vpc.testPocketApp_pocket_vpc_C4E157E3.id}" + ] + } + ] + } + }, + "aws_ssm_parameter": { + "testPocketApp_pocket_vpc_private_subnets_9D449563": { + "name": "/Shared/PrivateSubnets" + }, + "testPocketApp_pocket_vpc_public_subnets_B0B3A6AB": { + "name": "/Shared/PublicSubnets" + }, + "testPocketApp_pocket_vpc_vpc_ssm_param_235525D4": { + "name": "/Shared/Vpc" + } + }, + "aws_subnets": { + "testPocketApp_pocket_vpc_private_subnet_ids_EB1E3A65": { + "filter": [ + { + "name": "subnet-id", + "values": "\${split(\\",\\", data.aws_ssm_parameter.testPocketApp_pocket_vpc_private_subnets_9D449563.value)}" + }, + { + "name": "vpc-id", + "values": [ + "\${data.aws_vpc.testPocketApp_pocket_vpc_C4E157E3.id}" + ] + } + ] + }, + "testPocketApp_pocket_vpc_public_subnet_ids_01F0B902": { + "filter": [ + { + "name": "subnet-id", + "values": "\${split(\\",\\", data.aws_ssm_parameter.testPocketApp_pocket_vpc_public_subnets_B0B3A6AB.value)}" + }, + { + "name": "vpc-id", + "values": [ + "\${data.aws_vpc.testPocketApp_pocket_vpc_C4E157E3.id}" + ] + } + ] + } + }, + "aws_vpc": { + "testPocketApp_pocket_vpc_C4E157E3": { + "filter": [ + { + "name": "vpc-id", + "values": [ + "\${data.aws_ssm_parameter.testPocketApp_pocket_vpc_vpc_ssm_param_235525D4.value}" + ] + } + ] + } + } + }, + "resource": { + "aws_acm_certificate": { + "testPocketApp_alb_certificate_417C14FF": { + "domain_name": "testing.bowling.gov", + "lifecycle": { + "create_before_destroy": true + }, + "validation_method": "DNS" + } + }, + "aws_acm_certificate_validation": { + "testPocketApp_alb_certificate_certificate_validation_7D899C6A": { + "certificate_arn": "\${aws_acm_certificate.testPocketApp_alb_certificate_417C14FF.arn}", + "depends_on": [ + "aws_route53_record.testPocketApp_alb_certificate_certificate_record_3C49201F", + "aws_acm_certificate.testPocketApp_alb_certificate_417C14FF" + ], + "validation_record_fqdns": [ + "\${aws_route53_record.testPocketApp_alb_certificate_certificate_record_3C49201F.fqdn}" + ] + } + }, + "aws_alb": { + "testPocketApp_application_load_balancer_alb_D538DEEE": { + "internal": false, + "name_prefix": "TSTAPP", + "security_groups": [ + "\${aws_security_group.testPocketApp_application_load_balancer_alb_security_group_91A27046.id}" + ], + "subnets": "\${data.aws_subnets.testPocketApp_pocket_vpc_public_subnet_ids_01F0B902.ids}" + } + }, + "aws_alb_listener": { + "testPocketApp_listener_http_A9E227C2": { + "default_action": [ + { + "redirect": { + "port": "443", + "protocol": "HTTPS", + "status_code": "HTTP_301" + }, + "type": "redirect" + } + ], + "load_balancer_arn": "\${aws_alb.testPocketApp_application_load_balancer_alb_D538DEEE.arn}", + "port": 80, + "protocol": "HTTP" + }, + "testPocketApp_listener_https_7F49DE83": { + "certificate_arn": "\${aws_acm_certificate.testPocketApp_alb_certificate_417C14FF.arn}", + "default_action": [ + { + "fixed_response": { + "content_type": "text/plain", + "message_body": "", + "status_code": "503" + }, + "type": "fixed-response" + } + ], + "load_balancer_arn": "\${aws_alb.testPocketApp_application_load_balancer_alb_D538DEEE.arn}", + "port": 443, + "protocol": "HTTPS", + "ssl_policy": "ELBSecurityPolicy-TLS-1-1-2017-01" + } + }, + "aws_alb_listener_rule": { + "testPocketApp_ecs_service_listener_rule_C03BB04D": { + "action": [ + { + "target_group_arn": "\${aws_alb_target_group.testPocketApp_ecs_service_blue_target_group_ecs_target_group_A4B85885.arn}", + "type": "forward" + } + ], + "condition": [ + { + "path_pattern": { + "values": [ + "*" + ] + } + } + ], + "lifecycle": { + "ignore_changes": [ + "action" + ] + }, + "listener_arn": "\${aws_alb_listener.testPocketApp_listener_https_7F49DE83.arn}", + "priority": 1 + } + }, + "aws_alb_target_group": { + "testPocketApp_ecs_service_blue_target_group_ecs_target_group_A4B85885": { + "deregistration_delay": "120", + "health_check": { + "healthy_threshold": 5, + "interval": 15, + "path": "/test", + "protocol": "HTTP", + "unhealthy_threshold": 3 + }, + "name_prefix": "TSTAPP", + "port": 80, + "protocol": "HTTP", + "tags": { + "type": "blue" + }, + "target_type": "ip", + "vpc_id": "\${data.aws_vpc.testPocketApp_pocket_vpc_C4E157E3.id}" + } + }, + "aws_appautoscaling_policy": { + "testPocketApp_autoscaling_scale_in_policy_A163A235": { + "depends_on": [ + "aws_appautoscaling_target.testPocketApp_autoscaling_autoscaling_target_505E4EA1" + ], + "name": "testapp-ScaleInPolicy", + "policy_type": "StepScaling", + "resource_id": "\${aws_appautoscaling_target.testPocketApp_autoscaling_autoscaling_target_505E4EA1.resource_id}", + "scalable_dimension": "\${aws_appautoscaling_target.testPocketApp_autoscaling_autoscaling_target_505E4EA1.scalable_dimension}", + "service_namespace": "\${aws_appautoscaling_target.testPocketApp_autoscaling_autoscaling_target_505E4EA1.service_namespace}", + "step_scaling_policy_configuration": { + "adjustment_type": "ChangeInCapacity", + "cooldown": 60, + "metric_aggregation_type": "Average", + "step_adjustment": [ + { + "metric_interval_upper_bound": "0", + "scaling_adjustment": -1 + } + ] + }, + "target_tracking_scaling_policy_configuration": null + }, + "testPocketApp_autoscaling_scale_out_policy_075BFAC4": { + "depends_on": [ + "aws_appautoscaling_target.testPocketApp_autoscaling_autoscaling_target_505E4EA1" + ], + "name": "testapp-ScaleOutPolicy", + "policy_type": "StepScaling", + "resource_id": "\${aws_appautoscaling_target.testPocketApp_autoscaling_autoscaling_target_505E4EA1.resource_id}", + "scalable_dimension": "\${aws_appautoscaling_target.testPocketApp_autoscaling_autoscaling_target_505E4EA1.scalable_dimension}", + "service_namespace": "\${aws_appautoscaling_target.testPocketApp_autoscaling_autoscaling_target_505E4EA1.service_namespace}", + "step_scaling_policy_configuration": { + "adjustment_type": "ChangeInCapacity", + "cooldown": 60, + "metric_aggregation_type": "Average", + "step_adjustment": [ + { + "metric_interval_lower_bound": "0", + "scaling_adjustment": 2 + } + ] + }, + "target_tracking_scaling_policy_configuration": null + } + }, + "aws_appautoscaling_target": { + "testPocketApp_autoscaling_autoscaling_target_505E4EA1": { + "max_capacity": 2, + "min_capacity": 1, + "resource_id": "service/\${aws_ecs_cluster.testPocketApp_ecs_cluster_C3960066.name}/\${aws_ecs_service.testPocketApp_ecs_service_ecs-service_182DEA4C.name}", + "scalable_dimension": "ecs:service:DesiredCount", + "service_namespace": "ecs" + } + }, + "aws_cloudwatch_dashboard": { + "testPocketApp_cloudwatch-dashboard_26E9F70C": { + "dashboard_body": "{\\"widgets\\":[{\\"type\\":\\"metric\\",\\"x\\":0,\\"y\\":0,\\"width\\":12,\\"height\\":6,\\"properties\\":{\\"metrics\\":[[\\"AWS/ApplicationELB\\",\\"HTTPCode_Target_4XX_Count\\",\\"LoadBalancer\\",\\"\${aws_alb.testPocketApp_application_load_balancer_alb_D538DEEE.arn_suffix}\\",{\\"yAxis\\":\\"left\\",\\"color\\":\\"#ff7f0e\\"}],[\\".\\",\\"RequestCount\\",\\".\\",\\".\\",{\\"yAxis\\":\\"right\\",\\"color\\":\\"#1f77b4\\"}],[\\".\\",\\"HTTPCode_Target_5XX_Count\\",\\".\\",\\".\\",{\\"color\\":\\"#d62728\\"}],[\\".\\",\\"HTTPCode_Target_2XX_Count\\",\\".\\",\\".\\",{\\"yAxis\\":\\"right\\",\\"color\\":\\"#2ca02c\\"}]],\\"view\\":\\"timeSeries\\",\\"stacked\\":false,\\"region\\":\\"us-east-1\\",\\"period\\":60,\\"stat\\":\\"Sum\\",\\"title\\":\\"Target Requests\\"}},{\\"type\\":\\"metric\\",\\"x\\":12,\\"y\\":0,\\"width\\":12,\\"height\\":6,\\"properties\\":{\\"metrics\\":[[\\"AWS/ApplicationELB\\",\\"HTTPCode_ELB_4XX_Count\\",\\"LoadBalancer\\",\\"\${aws_alb.testPocketApp_application_load_balancer_alb_D538DEEE.arn_suffix}\\",{\\"yAxis\\":\\"left\\",\\"color\\":\\"#ff7f0e\\"}],[\\".\\",\\"RequestCount\\",\\".\\",\\".\\",{\\"yAxis\\":\\"right\\",\\"color\\":\\"#1f77b4\\"}],[\\".\\",\\"HTTPCode_ELB_5XX_Count\\",\\".\\",\\".\\",{\\"color\\":\\"#d62728\\"}]],\\"view\\":\\"timeSeries\\",\\"stacked\\":false,\\"region\\":\\"us-east-1\\",\\"period\\":60,\\"stat\\":\\"Sum\\",\\"title\\":\\"ALB Requests\\"}},{\\"type\\":\\"metric\\",\\"x\\":12,\\"y\\":6,\\"width\\":12,\\"height\\":6,\\"properties\\":{\\"metrics\\":[[\\"AWS/ApplicationELB\\",\\"TargetResponseTime\\",\\"LoadBalancer\\",\\"\${aws_alb.testPocketApp_application_load_balancer_alb_D538DEEE.arn_suffix}\\",{\\"label\\":\\"Average\\",\\"color\\":\\"#aec7e8\\"}],[\\"...\\",{\\"stat\\":\\"p95\\",\\"label\\":\\"p95\\",\\"color\\":\\"#ffbb78\\"}],[\\"...\\",{\\"stat\\":\\"p99\\",\\"label\\":\\"p99\\",\\"color\\":\\"#98df8a\\"}]],\\"view\\":\\"timeSeries\\",\\"stacked\\":false,\\"region\\":\\"us-east-1\\",\\"stat\\":\\"Average\\",\\"period\\":60}},{\\"type\\":\\"metric\\",\\"x\\":0,\\"y\\":6,\\"width\\":12,\\"height\\":6,\\"properties\\":{\\"metrics\\":[[\\"ECS/ContainerInsights\\",\\"RunningTaskCount\\",\\"ServiceName\\",\\"\${aws_ecs_service.testPocketApp_ecs_service_ecs-service_182DEA4C.name}\\",\\"ClusterName\\",\\"\${aws_ecs_cluster.testPocketApp_ecs_cluster_C3960066.name}\\",{\\"yAxis\\":\\"right\\",\\"color\\":\\"#c49c94\\"}],[\\"AWS/ECS\\",\\"CPUUtilization\\",\\".\\",\\".\\",\\".\\",\\".\\",{\\"color\\":\\"#f7b6d2\\"}],[\\".\\",\\"MemoryUtilization\\",\\".\\",\\".\\",\\".\\",\\".\\",{\\"color\\":\\"#c7c7c7\\"}]],\\"view\\":\\"timeSeries\\",\\"stacked\\":false,\\"region\\":\\"us-east-1\\",\\"stat\\":\\"Average\\",\\"period\\":60,\\"annotations\\":{\\"horizontal\\":[{\\"color\\":\\"#e377c2\\",\\"label\\":\\"CPU scale out\\",\\"value\\":45},{\\"color\\":\\"#c5b0d5\\",\\"label\\":\\"CPU scale in\\",\\"value\\":30}]},\\"title\\":\\"Service Load\\"}}]}", + "dashboard_name": "testapp", + "lifecycle": { + "ignore_changes": [ + "dashboard_body" + ] + } + } + }, + "aws_cloudwatch_metric_alarm": { + "testPocketApp_alarm-custom_F535C2DE": { + "alarm_name": "testapp-Alarm-Custom", + "comparison_operator": "GreaterThanThreshold", + "dimensions": { + "Test": "Alarm" + }, + "evaluation_periods": 1, + "metric_name": "Custom", + "namespace": "TM/Alarm", + "period": 300, + "statistic": "Sum", + "threshold": 500 + }, + "testPocketApp_autoscaling_scale_in_alarm_69B5F00D": { + "alarm_actions": [ + "\${aws_appautoscaling_policy.testPocketApp_autoscaling_scale_in_policy_A163A235.arn}" + ], + "alarm_description": "Alarm to reduce capacity if container CPU is low", + "alarm_name": "testapp Service Low CPU", + "comparison_operator": "LessThanThreshold", + "dimensions": { + "ClusterName": "\${aws_ecs_cluster.testPocketApp_ecs_cluster_C3960066.name}", + "ServiceName": "\${aws_ecs_service.testPocketApp_ecs_service_ecs-service_182DEA4C.name}" + }, + "evaluation_periods": 2, + "metric_name": "CPUUtilization", + "namespace": "AWS/ECS", + "period": 60, + "statistic": "Average", + "threshold": 30, + "treat_missing_data": "notBreaching" + }, + "testPocketApp_autoscaling_scale_out_alarm_4313FBE9": { + "alarm_actions": [ + "\${aws_appautoscaling_policy.testPocketApp_autoscaling_scale_out_policy_075BFAC4.arn}" + ], + "alarm_description": "Alarm to add capacity if container CPU is high", + "alarm_name": "testapp Service High CPU", + "comparison_operator": "GreaterThanThreshold", + "dimensions": { + "ClusterName": "\${aws_ecs_cluster.testPocketApp_ecs_cluster_C3960066.name}", + "ServiceName": "\${aws_ecs_service.testPocketApp_ecs_service_ecs-service_182DEA4C.name}" + }, + "evaluation_periods": 2, + "metric_name": "CPUUtilization", + "namespace": "AWS/ECS", + "period": 60, + "statistic": "Average", + "threshold": 45, + "treat_missing_data": "notBreaching" + } + }, + "aws_ecs_cluster": { + "testPocketApp_ecs_cluster_C3960066": { + "name": "testapp", + "setting": [ + { + "name": "containerInsights", + "value": "enabled" + } + ] + } + }, + "aws_ecs_service": { + "testPocketApp_ecs_service_ecs-service_182DEA4C": { + "cluster": "\${aws_ecs_cluster.testPocketApp_ecs_cluster_C3960066.arn}", + "depends_on": [ + "aws_alb_target_group.testPocketApp_ecs_service_blue_target_group_ecs_target_group_A4B85885", + "aws_alb_listener_rule.testPocketApp_ecs_service_listener_rule_C03BB04D" + ], + "deployment_controller": { + "type": "ECS" + }, + "deployment_maximum_percent": 200, + "deployment_minimum_healthy_percent": 100, + "desired_count": 2, + "launch_type": "FARGATE", + "lifecycle": { + "ignore_changes": [ + "desired_count", + "load_balancer" + ] + }, + "load_balancer": [ + { + "container_name": "main_container", + "container_port": 8675309, + "target_group_arn": "\${aws_alb_target_group.testPocketApp_ecs_service_blue_target_group_ecs_target_group_A4B85885.arn}" + } + ], + "name": "testapp", + "network_configuration": { + "security_groups": [ + "\${aws_security_group.testPocketApp_ecs_service_ecs_security_group_9C016FA8.id}" + ], + "subnets": "\${data.aws_subnets.testPocketApp_pocket_vpc_private_subnet_ids_EB1E3A65.ids}" + }, + "propagate_tags": "SERVICE", + "task_definition": "\${aws_ecs_task_definition.testPocketApp_ecs_service_ecs-task_A7E74E45.arn}" + } + }, + "aws_ecs_task_definition": { + "testPocketApp_ecs_service_ecs-task_A7E74E45": { + "container_definitions": "[]", + "cpu": "512", + "depends_on": [ + ], + "execution_role_arn": "\${aws_iam_role.testPocketApp_ecs_service_ecs-iam_ecs-execution-role_EB461A0C.arn}", + "family": "testapp", + "memory": "2048", + "network_mode": "awsvpc", + "requires_compatibilities": [ + "FARGATE" + ], + "task_role_arn": "\${aws_iam_role.testPocketApp_ecs_service_ecs-iam_ecs-task-role_7DB93963.arn}", + "volume": [ + ] + } + }, + "aws_iam_role": { + "testPocketApp_ecs_service_ecs-iam_ecs-execution-role_EB461A0C": { + "assume_role_policy": "\${data.aws_iam_policy_document.testPocketApp_ecs_service_ecs-iam_ecs-task-assume_ABB81680.json}", + "name": "testapp-TaskExecutionRole" + }, + "testPocketApp_ecs_service_ecs-iam_ecs-task-role_7DB93963": { + "assume_role_policy": "\${data.aws_iam_policy_document.testPocketApp_ecs_service_ecs-iam_ecs-task-assume_ABB81680.json}", + "name": "testapp-TaskRole" + } + }, + "aws_route53_record": { + "testPocketApp_alb_certificate_certificate_record_3C49201F": { + "depends_on": [ + "aws_acm_certificate.testPocketApp_alb_certificate_417C14FF" + ], + "name": "\${tolist(aws_acm_certificate.testPocketApp_alb_certificate_417C14FF.domain_validation_options).0.resource_record_name}", + "records": [ + "\${tolist(aws_acm_certificate.testPocketApp_alb_certificate_417C14FF.domain_validation_options).0.resource_record_value}" + ], + "ttl": 60, + "type": "\${tolist(aws_acm_certificate.testPocketApp_alb_certificate_417C14FF.domain_validation_options).0.resource_record_type}", + "zone_id": "\${aws_route53_zone.testPocketApp_base_dns_subhosted_zone_1E1E3243.zone_id}" + }, + "testPocketApp_alb_record_7B56ED33": { + "alias": { + "evaluate_target_health": true, + "name": "\${aws_alb.testPocketApp_application_load_balancer_alb_D538DEEE.dns_name}", + "zone_id": "\${aws_alb.testPocketApp_application_load_balancer_alb_D538DEEE.zone_id}" + }, + "lifecycle": { + "ignore_changes": [ + "weighted_routing_policy[0].weight" + ] + }, + "name": "testing.bowling.gov", + "set_identifier": "1", + "type": "A", + "weighted_routing_policy": { + "weight": 1 + }, + "zone_id": "\${aws_route53_zone.testPocketApp_base_dns_subhosted_zone_1E1E3243.zone_id}" + }, + "testPocketApp_base_dns_subhosted_zone_ns_497FFB83": { + "name": "testing.bowling.gov", + "records": "\${aws_route53_zone.testPocketApp_base_dns_subhosted_zone_1E1E3243.name_servers}", + "ttl": 86400, + "type": "NS", + "zone_id": "\${data.aws_route53_zone.testPocketApp_base_dns_main_hosted_zone_9442EEB0.zone_id}" + } + }, + "aws_route53_zone": { + "testPocketApp_base_dns_subhosted_zone_1E1E3243": { + "name": "testing.bowling.gov" + } + }, + "aws_security_group": { + "testPocketApp_application_load_balancer_alb_security_group_91A27046": { + "description": "External security group (Managed by Terraform)", + "egress": [ + { + "cidr_blocks": [ + "0.0.0.0/0" + ], + "description": "required", + "from_port": 0, + "ipv6_cidr_blocks": [ + ], + "prefix_list_ids": [ + ], + "protocol": "-1", + "security_groups": [ + ], + "self": null, + "to_port": 0 + } + ], + "ingress": [ + { + "cidr_blocks": [ + "0.0.0.0/0" + ], + "description": null, + "from_port": 443, + "ipv6_cidr_blocks": null, + "prefix_list_ids": null, + "protocol": "TCP", + "security_groups": null, + "self": null, + "to_port": 443 + }, + { + "cidr_blocks": [ + "0.0.0.0/0" + ], + "description": null, + "from_port": 80, + "ipv6_cidr_blocks": null, + "prefix_list_ids": null, + "protocol": "TCP", + "security_groups": null, + "self": null, + "to_port": 80 + } + ], + "lifecycle": { + "create_before_destroy": true + }, + "name_prefix": "testapp-HTTP/S Security Group", + "tags": { + "Name": "testapp-HTTP/S Security Group" + }, + "vpc_id": "\${data.aws_vpc.testPocketApp_pocket_vpc_C4E157E3.id}" + }, + "testPocketApp_ecs_service_ecs_security_group_9C016FA8": { + "description": "Internal ECS Security Group (Managed by Terraform)", + "egress": [ + { + "cidr_blocks": [ + "0.0.0.0/0" + ], + "description": "required", + "from_port": 0, + "ipv6_cidr_blocks": [ + ], + "prefix_list_ids": [ + ], + "protocol": "-1", + "security_groups": [ + ], + "self": null, + "to_port": 0 + } + ], + "ingress": [ + { + "cidr_blocks": [ + ], + "description": "required", + "from_port": 80, + "ipv6_cidr_blocks": [ + ], + "prefix_list_ids": [ + ], + "protocol": "TCP", + "security_groups": [ + "\${aws_security_group.testPocketApp_application_load_balancer_alb_security_group_91A27046.id}" + ], + "self": null, + "to_port": 8675309 + } + ], + "lifecycle": { + "create_before_destroy": true + }, + "name_prefix": "testapp-ECSSecurityGroup", + "vpc_id": "\${data.aws_vpc.testPocketApp_pocket_vpc_C4E157E3.id}" + } + } + } +}" +`; + +exports[`PocketALBApplication renders an application with autoscaling group and tags 1`] = ` +"{ + "data": { + "aws_caller_identity": { + "testPocketApp_pocket_vpc_current_identity_06D87057": { + } + }, + "aws_iam_policy_document": { + "testPocketApp_ecs_service_ecs-iam_ecs-task-assume_ABB81680": { + "statement": [ + { + "actions": [ + "sts:AssumeRole" + ], + "effect": "Allow", + "principals": [ + { + "identifiers": [ + "ecs-tasks.amazonaws.com" + ], + "type": "Service" + } + ] + } + ], + "version": "2012-10-17" + } + }, + "aws_kms_alias": { + "testPocketApp_pocket_vpc_secrets_manager_key_9FD2BC93": { + "name": "alias/aws/secretsmanager" + } + }, + "aws_region": { + "testPocketApp_pocket_vpc_current_region_8ED435E7": { + } + }, + "aws_route53_zone": { + "testPocketApp_base_dns_main_hosted_zone_9442EEB0": { + "name": "bowling.gov" + } + }, + "aws_security_groups": { + "testPocketApp_pocket_vpc_default_security_groups_40B4FC48": { + "filter": [ + { + "name": "group-name", + "values": [ + "default" + ] + }, + { + "name": "vpc-id", + "values": [ + "\${data.aws_vpc.testPocketApp_pocket_vpc_C4E157E3.id}" + ] + } + ] + }, + "testPocketApp_pocket_vpc_internal_security_groups_12F7F666": { + "filter": [ + { + "name": "group-name", + "values": [ + "pocket-vpc-internal" + ] + }, + { + "name": "vpc-id", + "values": [ + "\${data.aws_vpc.testPocketApp_pocket_vpc_C4E157E3.id}" + ] + } + ] + } + }, + "aws_ssm_parameter": { + "testPocketApp_pocket_vpc_private_subnets_9D449563": { + "name": "/Shared/PrivateSubnets" + }, + "testPocketApp_pocket_vpc_public_subnets_B0B3A6AB": { + "name": "/Shared/PublicSubnets" + }, + "testPocketApp_pocket_vpc_vpc_ssm_param_235525D4": { + "name": "/Shared/Vpc" + } + }, + "aws_subnets": { + "testPocketApp_pocket_vpc_private_subnet_ids_EB1E3A65": { + "filter": [ + { + "name": "subnet-id", + "values": "\${split(\\",\\", data.aws_ssm_parameter.testPocketApp_pocket_vpc_private_subnets_9D449563.value)}" + }, + { + "name": "vpc-id", + "values": [ + "\${data.aws_vpc.testPocketApp_pocket_vpc_C4E157E3.id}" + ] + } + ] + }, + "testPocketApp_pocket_vpc_public_subnet_ids_01F0B902": { + "filter": [ + { + "name": "subnet-id", + "values": "\${split(\\",\\", data.aws_ssm_parameter.testPocketApp_pocket_vpc_public_subnets_B0B3A6AB.value)}" + }, + { + "name": "vpc-id", + "values": [ + "\${data.aws_vpc.testPocketApp_pocket_vpc_C4E157E3.id}" + ] + } + ] + } + }, + "aws_vpc": { + "testPocketApp_pocket_vpc_C4E157E3": { + "filter": [ + { + "name": "vpc-id", + "values": [ + "\${data.aws_ssm_parameter.testPocketApp_pocket_vpc_vpc_ssm_param_235525D4.value}" + ] + } + ] + } + } + }, + "resource": { + "aws_acm_certificate": { + "testPocketApp_alb_certificate_417C14FF": { + "domain_name": "testing.bowling.gov", + "lifecycle": { + "create_before_destroy": true + }, + "tags": { + "hobby": "bowling", + "name": "thedude" + }, + "validation_method": "DNS" + } + }, + "aws_acm_certificate_validation": { + "testPocketApp_alb_certificate_certificate_validation_7D899C6A": { + "certificate_arn": "\${aws_acm_certificate.testPocketApp_alb_certificate_417C14FF.arn}", + "depends_on": [ + "aws_route53_record.testPocketApp_alb_certificate_certificate_record_3C49201F", + "aws_acm_certificate.testPocketApp_alb_certificate_417C14FF" + ], + "validation_record_fqdns": [ + "\${aws_route53_record.testPocketApp_alb_certificate_certificate_record_3C49201F.fqdn}" + ] + } + }, + "aws_alb": { + "testPocketApp_application_load_balancer_alb_D538DEEE": { + "internal": false, + "name_prefix": "TSTAPP", + "security_groups": [ + "\${aws_security_group.testPocketApp_application_load_balancer_alb_security_group_91A27046.id}" + ], + "subnets": "\${data.aws_subnets.testPocketApp_pocket_vpc_public_subnet_ids_01F0B902.ids}", + "tags": { + "hobby": "bowling", + "name": "thedude" + } + } + }, + "aws_alb_listener": { + "testPocketApp_listener_http_A9E227C2": { + "default_action": [ + { + "redirect": { + "port": "443", + "protocol": "HTTPS", + "status_code": "HTTP_301" + }, + "type": "redirect" + } + ], + "load_balancer_arn": "\${aws_alb.testPocketApp_application_load_balancer_alb_D538DEEE.arn}", + "port": 80, + "protocol": "HTTP", + "tags": { + "hobby": "bowling", + "name": "thedude" + } + }, + "testPocketApp_listener_https_7F49DE83": { + "certificate_arn": "\${aws_acm_certificate.testPocketApp_alb_certificate_417C14FF.arn}", + "default_action": [ + { + "fixed_response": { + "content_type": "text/plain", + "message_body": "", + "status_code": "503" + }, + "type": "fixed-response" + } + ], + "load_balancer_arn": "\${aws_alb.testPocketApp_application_load_balancer_alb_D538DEEE.arn}", + "port": 443, + "protocol": "HTTPS", + "ssl_policy": "ELBSecurityPolicy-TLS-1-1-2017-01", + "tags": { + "hobby": "bowling", + "name": "thedude" + } + } + }, + "aws_alb_listener_rule": { + "testPocketApp_ecs_service_listener_rule_C03BB04D": { + "action": [ + { + "target_group_arn": "\${aws_alb_target_group.testPocketApp_ecs_service_blue_target_group_ecs_target_group_A4B85885.arn}", + "type": "forward" + } + ], + "condition": [ + { + "path_pattern": { + "values": [ + "*" + ] + } + } + ], + "lifecycle": { + "ignore_changes": [ + "action" + ] + }, + "listener_arn": "\${aws_alb_listener.testPocketApp_listener_https_7F49DE83.arn}", + "priority": 1, + "tags": { + "hobby": "bowling", + "name": "thedude" + } + } + }, + "aws_alb_target_group": { + "testPocketApp_ecs_service_blue_target_group_ecs_target_group_A4B85885": { + "deregistration_delay": "120", + "health_check": { + "healthy_threshold": 5, + "interval": 15, + "path": "/test", + "protocol": "HTTP", + "unhealthy_threshold": 3 + }, + "name_prefix": "TSTAPP", + "port": 80, + "protocol": "HTTP", + "tags": { + "hobby": "bowling", + "name": "thedude", + "type": "blue" + }, + "target_type": "ip", + "vpc_id": "\${data.aws_vpc.testPocketApp_pocket_vpc_C4E157E3.id}" + } + }, + "aws_appautoscaling_policy": { + "testPocketApp_autoscaling_scale_in_policy_A163A235": { + "depends_on": [ + "aws_appautoscaling_target.testPocketApp_autoscaling_autoscaling_target_505E4EA1" + ], + "name": "testapp-ScaleInPolicy", + "policy_type": "StepScaling", + "resource_id": "\${aws_appautoscaling_target.testPocketApp_autoscaling_autoscaling_target_505E4EA1.resource_id}", + "scalable_dimension": "\${aws_appautoscaling_target.testPocketApp_autoscaling_autoscaling_target_505E4EA1.scalable_dimension}", + "service_namespace": "\${aws_appautoscaling_target.testPocketApp_autoscaling_autoscaling_target_505E4EA1.service_namespace}", + "step_scaling_policy_configuration": { + "adjustment_type": "ChangeInCapacity", + "cooldown": 60, + "metric_aggregation_type": "Average", + "step_adjustment": [ + { + "metric_interval_upper_bound": "0", + "scaling_adjustment": -1 + } + ] + }, + "target_tracking_scaling_policy_configuration": null + }, + "testPocketApp_autoscaling_scale_out_policy_075BFAC4": { + "depends_on": [ + "aws_appautoscaling_target.testPocketApp_autoscaling_autoscaling_target_505E4EA1" + ], + "name": "testapp-ScaleOutPolicy", + "policy_type": "StepScaling", + "resource_id": "\${aws_appautoscaling_target.testPocketApp_autoscaling_autoscaling_target_505E4EA1.resource_id}", + "scalable_dimension": "\${aws_appautoscaling_target.testPocketApp_autoscaling_autoscaling_target_505E4EA1.scalable_dimension}", + "service_namespace": "\${aws_appautoscaling_target.testPocketApp_autoscaling_autoscaling_target_505E4EA1.service_namespace}", + "step_scaling_policy_configuration": { + "adjustment_type": "ChangeInCapacity", + "cooldown": 60, + "metric_aggregation_type": "Average", + "step_adjustment": [ + { + "metric_interval_lower_bound": "0", + "scaling_adjustment": 2 + } + ] + }, + "target_tracking_scaling_policy_configuration": null + } + }, + "aws_appautoscaling_target": { + "testPocketApp_autoscaling_autoscaling_target_505E4EA1": { + "max_capacity": 2, + "min_capacity": 1, + "resource_id": "service/\${aws_ecs_cluster.testPocketApp_ecs_cluster_C3960066.name}/\${aws_ecs_service.testPocketApp_ecs_service_ecs-service_182DEA4C.name}", + "scalable_dimension": "ecs:service:DesiredCount", + "service_namespace": "ecs" + } + }, + "aws_cloudwatch_dashboard": { + "testPocketApp_cloudwatch-dashboard_26E9F70C": { + "dashboard_body": "{\\"widgets\\":[{\\"type\\":\\"metric\\",\\"x\\":0,\\"y\\":0,\\"width\\":12,\\"height\\":6,\\"properties\\":{\\"metrics\\":[[\\"AWS/ApplicationELB\\",\\"HTTPCode_Target_4XX_Count\\",\\"LoadBalancer\\",\\"\${aws_alb.testPocketApp_application_load_balancer_alb_D538DEEE.arn_suffix}\\",{\\"yAxis\\":\\"left\\",\\"color\\":\\"#ff7f0e\\"}],[\\".\\",\\"RequestCount\\",\\".\\",\\".\\",{\\"yAxis\\":\\"right\\",\\"color\\":\\"#1f77b4\\"}],[\\".\\",\\"HTTPCode_Target_5XX_Count\\",\\".\\",\\".\\",{\\"color\\":\\"#d62728\\"}],[\\".\\",\\"HTTPCode_Target_2XX_Count\\",\\".\\",\\".\\",{\\"yAxis\\":\\"right\\",\\"color\\":\\"#2ca02c\\"}]],\\"view\\":\\"timeSeries\\",\\"stacked\\":false,\\"region\\":\\"us-east-1\\",\\"period\\":60,\\"stat\\":\\"Sum\\",\\"title\\":\\"Target Requests\\"}},{\\"type\\":\\"metric\\",\\"x\\":12,\\"y\\":0,\\"width\\":12,\\"height\\":6,\\"properties\\":{\\"metrics\\":[[\\"AWS/ApplicationELB\\",\\"HTTPCode_ELB_4XX_Count\\",\\"LoadBalancer\\",\\"\${aws_alb.testPocketApp_application_load_balancer_alb_D538DEEE.arn_suffix}\\",{\\"yAxis\\":\\"left\\",\\"color\\":\\"#ff7f0e\\"}],[\\".\\",\\"RequestCount\\",\\".\\",\\".\\",{\\"yAxis\\":\\"right\\",\\"color\\":\\"#1f77b4\\"}],[\\".\\",\\"HTTPCode_ELB_5XX_Count\\",\\".\\",\\".\\",{\\"color\\":\\"#d62728\\"}]],\\"view\\":\\"timeSeries\\",\\"stacked\\":false,\\"region\\":\\"us-east-1\\",\\"period\\":60,\\"stat\\":\\"Sum\\",\\"title\\":\\"ALB Requests\\"}},{\\"type\\":\\"metric\\",\\"x\\":12,\\"y\\":6,\\"width\\":12,\\"height\\":6,\\"properties\\":{\\"metrics\\":[[\\"AWS/ApplicationELB\\",\\"TargetResponseTime\\",\\"LoadBalancer\\",\\"\${aws_alb.testPocketApp_application_load_balancer_alb_D538DEEE.arn_suffix}\\",{\\"label\\":\\"Average\\",\\"color\\":\\"#aec7e8\\"}],[\\"...\\",{\\"stat\\":\\"p95\\",\\"label\\":\\"p95\\",\\"color\\":\\"#ffbb78\\"}],[\\"...\\",{\\"stat\\":\\"p99\\",\\"label\\":\\"p99\\",\\"color\\":\\"#98df8a\\"}]],\\"view\\":\\"timeSeries\\",\\"stacked\\":false,\\"region\\":\\"us-east-1\\",\\"stat\\":\\"Average\\",\\"period\\":60}},{\\"type\\":\\"metric\\",\\"x\\":0,\\"y\\":6,\\"width\\":12,\\"height\\":6,\\"properties\\":{\\"metrics\\":[[\\"ECS/ContainerInsights\\",\\"RunningTaskCount\\",\\"ServiceName\\",\\"\${aws_ecs_service.testPocketApp_ecs_service_ecs-service_182DEA4C.name}\\",\\"ClusterName\\",\\"\${aws_ecs_cluster.testPocketApp_ecs_cluster_C3960066.name}\\",{\\"yAxis\\":\\"right\\",\\"color\\":\\"#c49c94\\"}],[\\"AWS/ECS\\",\\"CPUUtilization\\",\\".\\",\\".\\",\\".\\",\\".\\",{\\"color\\":\\"#f7b6d2\\"}],[\\".\\",\\"MemoryUtilization\\",\\".\\",\\".\\",\\".\\",\\".\\",{\\"color\\":\\"#c7c7c7\\"}]],\\"view\\":\\"timeSeries\\",\\"stacked\\":false,\\"region\\":\\"us-east-1\\",\\"stat\\":\\"Average\\",\\"period\\":60,\\"annotations\\":{\\"horizontal\\":[{\\"color\\":\\"#e377c2\\",\\"label\\":\\"CPU scale out\\",\\"value\\":45},{\\"color\\":\\"#c5b0d5\\",\\"label\\":\\"CPU scale in\\",\\"value\\":30}]},\\"title\\":\\"Service Load\\"}}]}", + "dashboard_name": "testapp", + "lifecycle": { + "ignore_changes": [ + "dashboard_body" + ] + } + } + }, + "aws_cloudwatch_metric_alarm": { + "testPocketApp_autoscaling_scale_in_alarm_69B5F00D": { + "alarm_actions": [ + "\${aws_appautoscaling_policy.testPocketApp_autoscaling_scale_in_policy_A163A235.arn}" + ], + "alarm_description": "Alarm to reduce capacity if container CPU is low", + "alarm_name": "testapp Service Low CPU", + "comparison_operator": "LessThanThreshold", + "dimensions": { + "ClusterName": "\${aws_ecs_cluster.testPocketApp_ecs_cluster_C3960066.name}", + "ServiceName": "\${aws_ecs_service.testPocketApp_ecs_service_ecs-service_182DEA4C.name}" + }, + "evaluation_periods": 2, + "metric_name": "CPUUtilization", + "namespace": "AWS/ECS", + "period": 60, + "statistic": "Average", + "tags": { + "hobby": "bowling", + "name": "thedude" + }, + "threshold": 30, + "treat_missing_data": "notBreaching" + }, + "testPocketApp_autoscaling_scale_out_alarm_4313FBE9": { + "alarm_actions": [ + "\${aws_appautoscaling_policy.testPocketApp_autoscaling_scale_out_policy_075BFAC4.arn}" + ], + "alarm_description": "Alarm to add capacity if container CPU is high", + "alarm_name": "testapp Service High CPU", + "comparison_operator": "GreaterThanThreshold", + "dimensions": { + "ClusterName": "\${aws_ecs_cluster.testPocketApp_ecs_cluster_C3960066.name}", + "ServiceName": "\${aws_ecs_service.testPocketApp_ecs_service_ecs-service_182DEA4C.name}" + }, + "evaluation_periods": 2, + "metric_name": "CPUUtilization", + "namespace": "AWS/ECS", + "period": 60, + "statistic": "Average", + "tags": { + "hobby": "bowling", + "name": "thedude" + }, + "threshold": 45, + "treat_missing_data": "notBreaching" + } + }, + "aws_ecs_cluster": { + "testPocketApp_ecs_cluster_C3960066": { + "name": "testapp", + "setting": [ + { + "name": "containerInsights", + "value": "enabled" + } + ], + "tags": { + "hobby": "bowling", + "name": "thedude" + } + } + }, + "aws_ecs_service": { + "testPocketApp_ecs_service_ecs-service_182DEA4C": { + "cluster": "\${aws_ecs_cluster.testPocketApp_ecs_cluster_C3960066.arn}", + "depends_on": [ + "aws_alb_target_group.testPocketApp_ecs_service_blue_target_group_ecs_target_group_A4B85885", + "aws_alb_listener_rule.testPocketApp_ecs_service_listener_rule_C03BB04D" + ], + "deployment_controller": { + "type": "ECS" + }, + "deployment_maximum_percent": 200, + "deployment_minimum_healthy_percent": 100, + "desired_count": 2, + "launch_type": "FARGATE", + "lifecycle": { + "ignore_changes": [ + "desired_count", + "load_balancer" + ] + }, + "load_balancer": [ + { + "container_name": "main_container", + "container_port": 8675309, + "target_group_arn": "\${aws_alb_target_group.testPocketApp_ecs_service_blue_target_group_ecs_target_group_A4B85885.arn}" + } + ], + "name": "testapp", + "network_configuration": { + "security_groups": [ + "\${aws_security_group.testPocketApp_ecs_service_ecs_security_group_9C016FA8.id}" + ], + "subnets": "\${data.aws_subnets.testPocketApp_pocket_vpc_private_subnet_ids_EB1E3A65.ids}" + }, + "propagate_tags": "SERVICE", + "tags": { + "hobby": "bowling", + "name": "thedude" + }, + "task_definition": "\${aws_ecs_task_definition.testPocketApp_ecs_service_ecs-task_A7E74E45.arn}" + } + }, + "aws_ecs_task_definition": { + "testPocketApp_ecs_service_ecs-task_A7E74E45": { + "container_definitions": "[]", + "cpu": "512", + "depends_on": [ + ], + "execution_role_arn": "\${aws_iam_role.testPocketApp_ecs_service_ecs-iam_ecs-execution-role_EB461A0C.arn}", + "family": "testapp", + "memory": "2048", + "network_mode": "awsvpc", + "requires_compatibilities": [ + "FARGATE" + ], + "tags": { + "hobby": "bowling", + "name": "thedude" + }, + "task_role_arn": "\${aws_iam_role.testPocketApp_ecs_service_ecs-iam_ecs-task-role_7DB93963.arn}", + "volume": [ + ] + } + }, + "aws_iam_role": { + "testPocketApp_ecs_service_ecs-iam_ecs-execution-role_EB461A0C": { + "assume_role_policy": "\${data.aws_iam_policy_document.testPocketApp_ecs_service_ecs-iam_ecs-task-assume_ABB81680.json}", + "name": "testapp-TaskExecutionRole", + "tags": { + "hobby": "bowling", + "name": "thedude" + } + }, + "testPocketApp_ecs_service_ecs-iam_ecs-task-role_7DB93963": { + "assume_role_policy": "\${data.aws_iam_policy_document.testPocketApp_ecs_service_ecs-iam_ecs-task-assume_ABB81680.json}", + "name": "testapp-TaskRole", + "tags": { + "hobby": "bowling", + "name": "thedude" + } + } + }, + "aws_route53_record": { + "testPocketApp_alb_certificate_certificate_record_3C49201F": { + "depends_on": [ + "aws_acm_certificate.testPocketApp_alb_certificate_417C14FF" + ], + "name": "\${tolist(aws_acm_certificate.testPocketApp_alb_certificate_417C14FF.domain_validation_options).0.resource_record_name}", + "records": [ + "\${tolist(aws_acm_certificate.testPocketApp_alb_certificate_417C14FF.domain_validation_options).0.resource_record_value}" + ], + "ttl": 60, + "type": "\${tolist(aws_acm_certificate.testPocketApp_alb_certificate_417C14FF.domain_validation_options).0.resource_record_type}", + "zone_id": "\${aws_route53_zone.testPocketApp_base_dns_subhosted_zone_1E1E3243.zone_id}" + }, + "testPocketApp_alb_record_7B56ED33": { + "alias": { + "evaluate_target_health": true, + "name": "\${aws_alb.testPocketApp_application_load_balancer_alb_D538DEEE.dns_name}", + "zone_id": "\${aws_alb.testPocketApp_application_load_balancer_alb_D538DEEE.zone_id}" + }, + "lifecycle": { + "ignore_changes": [ + "weighted_routing_policy[0].weight" + ] + }, + "name": "testing.bowling.gov", + "set_identifier": "1", + "type": "A", + "weighted_routing_policy": { + "weight": 1 + }, + "zone_id": "\${aws_route53_zone.testPocketApp_base_dns_subhosted_zone_1E1E3243.zone_id}" + }, + "testPocketApp_base_dns_subhosted_zone_ns_497FFB83": { + "name": "testing.bowling.gov", + "records": "\${aws_route53_zone.testPocketApp_base_dns_subhosted_zone_1E1E3243.name_servers}", + "ttl": 86400, + "type": "NS", + "zone_id": "\${data.aws_route53_zone.testPocketApp_base_dns_main_hosted_zone_9442EEB0.zone_id}" + } + }, + "aws_route53_zone": { + "testPocketApp_base_dns_subhosted_zone_1E1E3243": { + "name": "testing.bowling.gov", + "tags": { + "hobby": "bowling", + "name": "thedude" + } + } + }, + "aws_security_group": { + "testPocketApp_application_load_balancer_alb_security_group_91A27046": { + "description": "External security group (Managed by Terraform)", + "egress": [ + { + "cidr_blocks": [ + "0.0.0.0/0" + ], + "description": "required", + "from_port": 0, + "ipv6_cidr_blocks": [ + ], + "prefix_list_ids": [ + ], + "protocol": "-1", + "security_groups": [ + ], + "self": null, + "to_port": 0 + } + ], + "ingress": [ + { + "cidr_blocks": [ + "0.0.0.0/0" + ], + "description": null, + "from_port": 443, + "ipv6_cidr_blocks": null, + "prefix_list_ids": null, + "protocol": "TCP", + "security_groups": null, + "self": null, + "to_port": 443 + }, + { + "cidr_blocks": [ + "0.0.0.0/0" + ], + "description": null, + "from_port": 80, + "ipv6_cidr_blocks": null, + "prefix_list_ids": null, + "protocol": "TCP", + "security_groups": null, + "self": null, + "to_port": 80 + } + ], + "lifecycle": { + "create_before_destroy": true + }, + "name_prefix": "testapp-HTTP/S Security Group", + "tags": { + "Name": "testapp-HTTP/S Security Group", + "hobby": "bowling", + "name": "thedude" + }, + "vpc_id": "\${data.aws_vpc.testPocketApp_pocket_vpc_C4E157E3.id}" + }, + "testPocketApp_ecs_service_ecs_security_group_9C016FA8": { + "description": "Internal ECS Security Group (Managed by Terraform)", + "egress": [ + { + "cidr_blocks": [ + "0.0.0.0/0" + ], + "description": "required", + "from_port": 0, + "ipv6_cidr_blocks": [ + ], + "prefix_list_ids": [ + ], + "protocol": "-1", + "security_groups": [ + ], + "self": null, + "to_port": 0 + } + ], + "ingress": [ + { + "cidr_blocks": [ + ], + "description": "required", + "from_port": 80, + "ipv6_cidr_blocks": [ + ], + "prefix_list_ids": [ + ], + "protocol": "TCP", + "security_groups": [ + "\${aws_security_group.testPocketApp_application_load_balancer_alb_security_group_91A27046.id}" + ], + "self": null, + "to_port": 8675309 + } + ], + "lifecycle": { + "create_before_destroy": true + }, + "name_prefix": "testapp-ECSSecurityGroup", + "tags": { + "hobby": "bowling", + "name": "thedude" + }, + "vpc_id": "\${data.aws_vpc.testPocketApp_pocket_vpc_C4E157E3.id}" + } + } + } +}" +`; + +exports[`PocketALBApplication renders an application with custom task sizes 1`] = ` +"{ + "data": { + "aws_caller_identity": { + "testPocketApp_pocket_vpc_current_identity_06D87057": { + } + }, + "aws_iam_policy_document": { + "testPocketApp_ecs_service_ecs-iam_ecs-task-assume_ABB81680": { + "statement": [ + { + "actions": [ + "sts:AssumeRole" + ], + "effect": "Allow", + "principals": [ + { + "identifiers": [ + "ecs-tasks.amazonaws.com" + ], + "type": "Service" + } + ] + } + ], + "version": "2012-10-17" + } + }, + "aws_kms_alias": { + "testPocketApp_pocket_vpc_secrets_manager_key_9FD2BC93": { + "name": "alias/aws/secretsmanager" + } + }, + "aws_region": { + "testPocketApp_pocket_vpc_current_region_8ED435E7": { + } + }, + "aws_route53_zone": { + "testPocketApp_base_dns_main_hosted_zone_9442EEB0": { + "name": "bowling.gov" + } + }, + "aws_security_groups": { + "testPocketApp_pocket_vpc_default_security_groups_40B4FC48": { + "filter": [ + { + "name": "group-name", + "values": [ + "default" + ] + }, + { + "name": "vpc-id", + "values": [ + "\${data.aws_vpc.testPocketApp_pocket_vpc_C4E157E3.id}" + ] + } + ] + }, + "testPocketApp_pocket_vpc_internal_security_groups_12F7F666": { + "filter": [ + { + "name": "group-name", + "values": [ + "pocket-vpc-internal" + ] + }, + { + "name": "vpc-id", + "values": [ + "\${data.aws_vpc.testPocketApp_pocket_vpc_C4E157E3.id}" + ] + } + ] + } + }, + "aws_ssm_parameter": { + "testPocketApp_pocket_vpc_private_subnets_9D449563": { + "name": "/Shared/PrivateSubnets" + }, + "testPocketApp_pocket_vpc_public_subnets_B0B3A6AB": { + "name": "/Shared/PublicSubnets" + }, + "testPocketApp_pocket_vpc_vpc_ssm_param_235525D4": { + "name": "/Shared/Vpc" + } + }, + "aws_subnets": { + "testPocketApp_pocket_vpc_private_subnet_ids_EB1E3A65": { + "filter": [ + { + "name": "subnet-id", + "values": "\${split(\\",\\", data.aws_ssm_parameter.testPocketApp_pocket_vpc_private_subnets_9D449563.value)}" + }, + { + "name": "vpc-id", + "values": [ + "\${data.aws_vpc.testPocketApp_pocket_vpc_C4E157E3.id}" + ] + } + ] + }, + "testPocketApp_pocket_vpc_public_subnet_ids_01F0B902": { + "filter": [ + { + "name": "subnet-id", + "values": "\${split(\\",\\", data.aws_ssm_parameter.testPocketApp_pocket_vpc_public_subnets_B0B3A6AB.value)}" + }, + { + "name": "vpc-id", + "values": [ + "\${data.aws_vpc.testPocketApp_pocket_vpc_C4E157E3.id}" + ] + } + ] + } + }, + "aws_vpc": { + "testPocketApp_pocket_vpc_C4E157E3": { + "filter": [ + { + "name": "vpc-id", + "values": [ + "\${data.aws_ssm_parameter.testPocketApp_pocket_vpc_vpc_ssm_param_235525D4.value}" + ] + } + ] + } + } + }, + "resource": { + "aws_acm_certificate": { + "testPocketApp_alb_certificate_417C14FF": { + "domain_name": "testing.bowling.gov", + "lifecycle": { + "create_before_destroy": true + }, + "validation_method": "DNS" + } + }, + "aws_acm_certificate_validation": { + "testPocketApp_alb_certificate_certificate_validation_7D899C6A": { + "certificate_arn": "\${aws_acm_certificate.testPocketApp_alb_certificate_417C14FF.arn}", + "depends_on": [ + "aws_route53_record.testPocketApp_alb_certificate_certificate_record_3C49201F", + "aws_acm_certificate.testPocketApp_alb_certificate_417C14FF" + ], + "validation_record_fqdns": [ + "\${aws_route53_record.testPocketApp_alb_certificate_certificate_record_3C49201F.fqdn}" + ] + } + }, + "aws_alb": { + "testPocketApp_application_load_balancer_alb_D538DEEE": { + "internal": false, + "name_prefix": "TSTAPP", + "security_groups": [ + "\${aws_security_group.testPocketApp_application_load_balancer_alb_security_group_91A27046.id}" + ], + "subnets": "\${data.aws_subnets.testPocketApp_pocket_vpc_public_subnet_ids_01F0B902.ids}" + } + }, + "aws_alb_listener": { + "testPocketApp_listener_http_A9E227C2": { + "default_action": [ + { + "redirect": { + "port": "443", + "protocol": "HTTPS", + "status_code": "HTTP_301" + }, + "type": "redirect" + } + ], + "load_balancer_arn": "\${aws_alb.testPocketApp_application_load_balancer_alb_D538DEEE.arn}", + "port": 80, + "protocol": "HTTP" + }, + "testPocketApp_listener_https_7F49DE83": { + "certificate_arn": "\${aws_acm_certificate.testPocketApp_alb_certificate_417C14FF.arn}", + "default_action": [ + { + "fixed_response": { + "content_type": "text/plain", + "message_body": "", + "status_code": "503" + }, + "type": "fixed-response" + } + ], + "load_balancer_arn": "\${aws_alb.testPocketApp_application_load_balancer_alb_D538DEEE.arn}", + "port": 443, + "protocol": "HTTPS", + "ssl_policy": "ELBSecurityPolicy-TLS-1-1-2017-01" + } + }, + "aws_alb_listener_rule": { + "testPocketApp_ecs_service_listener_rule_C03BB04D": { + "action": [ + { + "target_group_arn": "\${aws_alb_target_group.testPocketApp_ecs_service_blue_target_group_ecs_target_group_A4B85885.arn}", + "type": "forward" + } + ], + "condition": [ + { + "path_pattern": { + "values": [ + "*" + ] + } + } + ], + "lifecycle": { + "ignore_changes": [ + "action" + ] + }, + "listener_arn": "\${aws_alb_listener.testPocketApp_listener_https_7F49DE83.arn}", + "priority": 1 + } + }, + "aws_alb_target_group": { + "testPocketApp_ecs_service_blue_target_group_ecs_target_group_A4B85885": { + "deregistration_delay": "120", + "health_check": { + "healthy_threshold": 5, + "interval": 15, + "path": "/test", + "protocol": "HTTP", + "unhealthy_threshold": 3 + }, + "name_prefix": "TSTAPP", + "port": 80, + "protocol": "HTTP", + "tags": { + "type": "blue" + }, + "target_type": "ip", + "vpc_id": "\${data.aws_vpc.testPocketApp_pocket_vpc_C4E157E3.id}" + } + }, + "aws_appautoscaling_policy": { + "testPocketApp_autoscaling_scale_in_policy_A163A235": { + "depends_on": [ + "aws_appautoscaling_target.testPocketApp_autoscaling_autoscaling_target_505E4EA1" + ], + "name": "testapp-ScaleInPolicy", + "policy_type": "StepScaling", + "resource_id": "\${aws_appautoscaling_target.testPocketApp_autoscaling_autoscaling_target_505E4EA1.resource_id}", + "scalable_dimension": "\${aws_appautoscaling_target.testPocketApp_autoscaling_autoscaling_target_505E4EA1.scalable_dimension}", + "service_namespace": "\${aws_appautoscaling_target.testPocketApp_autoscaling_autoscaling_target_505E4EA1.service_namespace}", + "step_scaling_policy_configuration": { + "adjustment_type": "ChangeInCapacity", + "cooldown": 60, + "metric_aggregation_type": "Average", + "step_adjustment": [ + { + "metric_interval_upper_bound": "0", + "scaling_adjustment": -1 + } + ] + }, + "target_tracking_scaling_policy_configuration": null + }, + "testPocketApp_autoscaling_scale_out_policy_075BFAC4": { + "depends_on": [ + "aws_appautoscaling_target.testPocketApp_autoscaling_autoscaling_target_505E4EA1" + ], + "name": "testapp-ScaleOutPolicy", + "policy_type": "StepScaling", + "resource_id": "\${aws_appautoscaling_target.testPocketApp_autoscaling_autoscaling_target_505E4EA1.resource_id}", + "scalable_dimension": "\${aws_appautoscaling_target.testPocketApp_autoscaling_autoscaling_target_505E4EA1.scalable_dimension}", + "service_namespace": "\${aws_appautoscaling_target.testPocketApp_autoscaling_autoscaling_target_505E4EA1.service_namespace}", + "step_scaling_policy_configuration": { + "adjustment_type": "ChangeInCapacity", + "cooldown": 60, + "metric_aggregation_type": "Average", + "step_adjustment": [ + { + "metric_interval_lower_bound": "0", + "scaling_adjustment": 2 + } + ] + }, + "target_tracking_scaling_policy_configuration": null + } + }, + "aws_appautoscaling_target": { + "testPocketApp_autoscaling_autoscaling_target_505E4EA1": { + "max_capacity": 2, + "min_capacity": 1, + "resource_id": "service/\${aws_ecs_cluster.testPocketApp_ecs_cluster_C3960066.name}/\${aws_ecs_service.testPocketApp_ecs_service_ecs-service_182DEA4C.name}", + "scalable_dimension": "ecs:service:DesiredCount", + "service_namespace": "ecs" + } + }, + "aws_cloudwatch_dashboard": { + "testPocketApp_cloudwatch-dashboard_26E9F70C": { + "dashboard_body": "{\\"widgets\\":[{\\"type\\":\\"metric\\",\\"x\\":0,\\"y\\":0,\\"width\\":12,\\"height\\":6,\\"properties\\":{\\"metrics\\":[[\\"AWS/ApplicationELB\\",\\"HTTPCode_Target_4XX_Count\\",\\"LoadBalancer\\",\\"\${aws_alb.testPocketApp_application_load_balancer_alb_D538DEEE.arn_suffix}\\",{\\"yAxis\\":\\"left\\",\\"color\\":\\"#ff7f0e\\"}],[\\".\\",\\"RequestCount\\",\\".\\",\\".\\",{\\"yAxis\\":\\"right\\",\\"color\\":\\"#1f77b4\\"}],[\\".\\",\\"HTTPCode_Target_5XX_Count\\",\\".\\",\\".\\",{\\"color\\":\\"#d62728\\"}],[\\".\\",\\"HTTPCode_Target_2XX_Count\\",\\".\\",\\".\\",{\\"yAxis\\":\\"right\\",\\"color\\":\\"#2ca02c\\"}]],\\"view\\":\\"timeSeries\\",\\"stacked\\":false,\\"region\\":\\"us-east-1\\",\\"period\\":60,\\"stat\\":\\"Sum\\",\\"title\\":\\"Target Requests\\"}},{\\"type\\":\\"metric\\",\\"x\\":12,\\"y\\":0,\\"width\\":12,\\"height\\":6,\\"properties\\":{\\"metrics\\":[[\\"AWS/ApplicationELB\\",\\"HTTPCode_ELB_4XX_Count\\",\\"LoadBalancer\\",\\"\${aws_alb.testPocketApp_application_load_balancer_alb_D538DEEE.arn_suffix}\\",{\\"yAxis\\":\\"left\\",\\"color\\":\\"#ff7f0e\\"}],[\\".\\",\\"RequestCount\\",\\".\\",\\".\\",{\\"yAxis\\":\\"right\\",\\"color\\":\\"#1f77b4\\"}],[\\".\\",\\"HTTPCode_ELB_5XX_Count\\",\\".\\",\\".\\",{\\"color\\":\\"#d62728\\"}]],\\"view\\":\\"timeSeries\\",\\"stacked\\":false,\\"region\\":\\"us-east-1\\",\\"period\\":60,\\"stat\\":\\"Sum\\",\\"title\\":\\"ALB Requests\\"}},{\\"type\\":\\"metric\\",\\"x\\":12,\\"y\\":6,\\"width\\":12,\\"height\\":6,\\"properties\\":{\\"metrics\\":[[\\"AWS/ApplicationELB\\",\\"TargetResponseTime\\",\\"LoadBalancer\\",\\"\${aws_alb.testPocketApp_application_load_balancer_alb_D538DEEE.arn_suffix}\\",{\\"label\\":\\"Average\\",\\"color\\":\\"#aec7e8\\"}],[\\"...\\",{\\"stat\\":\\"p95\\",\\"label\\":\\"p95\\",\\"color\\":\\"#ffbb78\\"}],[\\"...\\",{\\"stat\\":\\"p99\\",\\"label\\":\\"p99\\",\\"color\\":\\"#98df8a\\"}]],\\"view\\":\\"timeSeries\\",\\"stacked\\":false,\\"region\\":\\"us-east-1\\",\\"stat\\":\\"Average\\",\\"period\\":60}},{\\"type\\":\\"metric\\",\\"x\\":0,\\"y\\":6,\\"width\\":12,\\"height\\":6,\\"properties\\":{\\"metrics\\":[[\\"ECS/ContainerInsights\\",\\"RunningTaskCount\\",\\"ServiceName\\",\\"\${aws_ecs_service.testPocketApp_ecs_service_ecs-service_182DEA4C.name}\\",\\"ClusterName\\",\\"\${aws_ecs_cluster.testPocketApp_ecs_cluster_C3960066.name}\\",{\\"yAxis\\":\\"right\\",\\"color\\":\\"#c49c94\\"}],[\\"AWS/ECS\\",\\"CPUUtilization\\",\\".\\",\\".\\",\\".\\",\\".\\",{\\"color\\":\\"#f7b6d2\\"}],[\\".\\",\\"MemoryUtilization\\",\\".\\",\\".\\",\\".\\",\\".\\",{\\"color\\":\\"#c7c7c7\\"}]],\\"view\\":\\"timeSeries\\",\\"stacked\\":false,\\"region\\":\\"us-east-1\\",\\"stat\\":\\"Average\\",\\"period\\":60,\\"annotations\\":{\\"horizontal\\":[{\\"color\\":\\"#e377c2\\",\\"label\\":\\"CPU scale out\\",\\"value\\":45},{\\"color\\":\\"#c5b0d5\\",\\"label\\":\\"CPU scale in\\",\\"value\\":30}]},\\"title\\":\\"Service Load\\"}}]}", + "dashboard_name": "testapp", + "lifecycle": { + "ignore_changes": [ + "dashboard_body" + ] + } + } + }, + "aws_cloudwatch_metric_alarm": { + "testPocketApp_autoscaling_scale_in_alarm_69B5F00D": { + "alarm_actions": [ + "\${aws_appautoscaling_policy.testPocketApp_autoscaling_scale_in_policy_A163A235.arn}" + ], + "alarm_description": "Alarm to reduce capacity if container CPU is low", + "alarm_name": "testapp Service Low CPU", + "comparison_operator": "LessThanThreshold", + "dimensions": { + "ClusterName": "\${aws_ecs_cluster.testPocketApp_ecs_cluster_C3960066.name}", + "ServiceName": "\${aws_ecs_service.testPocketApp_ecs_service_ecs-service_182DEA4C.name}" + }, + "evaluation_periods": 2, + "metric_name": "CPUUtilization", + "namespace": "AWS/ECS", + "period": 60, + "statistic": "Average", + "threshold": 30, + "treat_missing_data": "notBreaching" + }, + "testPocketApp_autoscaling_scale_out_alarm_4313FBE9": { + "alarm_actions": [ + "\${aws_appautoscaling_policy.testPocketApp_autoscaling_scale_out_policy_075BFAC4.arn}" + ], + "alarm_description": "Alarm to add capacity if container CPU is high", + "alarm_name": "testapp Service High CPU", + "comparison_operator": "GreaterThanThreshold", + "dimensions": { + "ClusterName": "\${aws_ecs_cluster.testPocketApp_ecs_cluster_C3960066.name}", + "ServiceName": "\${aws_ecs_service.testPocketApp_ecs_service_ecs-service_182DEA4C.name}" + }, + "evaluation_periods": 2, + "metric_name": "CPUUtilization", + "namespace": "AWS/ECS", + "period": 60, + "statistic": "Average", + "threshold": 45, + "treat_missing_data": "notBreaching" + } + }, + "aws_ecs_cluster": { + "testPocketApp_ecs_cluster_C3960066": { + "name": "testapp", + "setting": [ + { + "name": "containerInsights", + "value": "enabled" + } + ] + } + }, + "aws_ecs_service": { + "testPocketApp_ecs_service_ecs-service_182DEA4C": { + "cluster": "\${aws_ecs_cluster.testPocketApp_ecs_cluster_C3960066.arn}", + "depends_on": [ + "aws_alb_target_group.testPocketApp_ecs_service_blue_target_group_ecs_target_group_A4B85885", + "aws_alb_listener_rule.testPocketApp_ecs_service_listener_rule_C03BB04D" + ], + "deployment_controller": { + "type": "ECS" + }, + "deployment_maximum_percent": 200, + "deployment_minimum_healthy_percent": 100, + "desired_count": 2, + "launch_type": "FARGATE", + "lifecycle": { + "ignore_changes": [ + "desired_count", + "load_balancer" + ] + }, + "load_balancer": [ + { + "container_name": "main_container", + "container_port": 8675309, + "target_group_arn": "\${aws_alb_target_group.testPocketApp_ecs_service_blue_target_group_ecs_target_group_A4B85885.arn}" + } + ], + "name": "testapp", + "network_configuration": { + "security_groups": [ + "\${aws_security_group.testPocketApp_ecs_service_ecs_security_group_9C016FA8.id}" + ], + "subnets": "\${data.aws_subnets.testPocketApp_pocket_vpc_private_subnet_ids_EB1E3A65.ids}" + }, + "propagate_tags": "SERVICE", + "task_definition": "\${aws_ecs_task_definition.testPocketApp_ecs_service_ecs-task_A7E74E45.arn}" + } + }, + "aws_ecs_task_definition": { + "testPocketApp_ecs_service_ecs-task_A7E74E45": { + "container_definitions": "[]", + "cpu": "8675", + "depends_on": [ + ], + "execution_role_arn": "\${aws_iam_role.testPocketApp_ecs_service_ecs-iam_ecs-execution-role_EB461A0C.arn}", + "family": "testapp", + "memory": "309", + "network_mode": "awsvpc", + "requires_compatibilities": [ + "FARGATE" + ], + "task_role_arn": "\${aws_iam_role.testPocketApp_ecs_service_ecs-iam_ecs-task-role_7DB93963.arn}", + "volume": [ + ] + } + }, + "aws_iam_role": { + "testPocketApp_ecs_service_ecs-iam_ecs-execution-role_EB461A0C": { + "assume_role_policy": "\${data.aws_iam_policy_document.testPocketApp_ecs_service_ecs-iam_ecs-task-assume_ABB81680.json}", + "name": "testapp-TaskExecutionRole" + }, + "testPocketApp_ecs_service_ecs-iam_ecs-task-role_7DB93963": { + "assume_role_policy": "\${data.aws_iam_policy_document.testPocketApp_ecs_service_ecs-iam_ecs-task-assume_ABB81680.json}", + "name": "testapp-TaskRole" + } + }, + "aws_route53_record": { + "testPocketApp_alb_certificate_certificate_record_3C49201F": { + "depends_on": [ + "aws_acm_certificate.testPocketApp_alb_certificate_417C14FF" + ], + "name": "\${tolist(aws_acm_certificate.testPocketApp_alb_certificate_417C14FF.domain_validation_options).0.resource_record_name}", + "records": [ + "\${tolist(aws_acm_certificate.testPocketApp_alb_certificate_417C14FF.domain_validation_options).0.resource_record_value}" + ], + "ttl": 60, + "type": "\${tolist(aws_acm_certificate.testPocketApp_alb_certificate_417C14FF.domain_validation_options).0.resource_record_type}", + "zone_id": "\${aws_route53_zone.testPocketApp_base_dns_subhosted_zone_1E1E3243.zone_id}" + }, + "testPocketApp_alb_record_7B56ED33": { + "alias": { + "evaluate_target_health": true, + "name": "\${aws_alb.testPocketApp_application_load_balancer_alb_D538DEEE.dns_name}", + "zone_id": "\${aws_alb.testPocketApp_application_load_balancer_alb_D538DEEE.zone_id}" + }, + "lifecycle": { + "ignore_changes": [ + "weighted_routing_policy[0].weight" + ] + }, + "name": "testing.bowling.gov", + "set_identifier": "1", + "type": "A", + "weighted_routing_policy": { + "weight": 1 + }, + "zone_id": "\${aws_route53_zone.testPocketApp_base_dns_subhosted_zone_1E1E3243.zone_id}" + }, + "testPocketApp_base_dns_subhosted_zone_ns_497FFB83": { + "name": "testing.bowling.gov", + "records": "\${aws_route53_zone.testPocketApp_base_dns_subhosted_zone_1E1E3243.name_servers}", + "ttl": 86400, + "type": "NS", + "zone_id": "\${data.aws_route53_zone.testPocketApp_base_dns_main_hosted_zone_9442EEB0.zone_id}" + } + }, + "aws_route53_zone": { + "testPocketApp_base_dns_subhosted_zone_1E1E3243": { + "name": "testing.bowling.gov" + } + }, + "aws_security_group": { + "testPocketApp_application_load_balancer_alb_security_group_91A27046": { + "description": "External security group (Managed by Terraform)", + "egress": [ + { + "cidr_blocks": [ + "0.0.0.0/0" + ], + "description": "required", + "from_port": 0, + "ipv6_cidr_blocks": [ + ], + "prefix_list_ids": [ + ], + "protocol": "-1", + "security_groups": [ + ], + "self": null, + "to_port": 0 + } + ], + "ingress": [ + { + "cidr_blocks": [ + "0.0.0.0/0" + ], + "description": null, + "from_port": 443, + "ipv6_cidr_blocks": null, + "prefix_list_ids": null, + "protocol": "TCP", + "security_groups": null, + "self": null, + "to_port": 443 + }, + { + "cidr_blocks": [ + "0.0.0.0/0" + ], + "description": null, + "from_port": 80, + "ipv6_cidr_blocks": null, + "prefix_list_ids": null, + "protocol": "TCP", + "security_groups": null, + "self": null, + "to_port": 80 + } + ], + "lifecycle": { + "create_before_destroy": true + }, + "name_prefix": "testapp-HTTP/S Security Group", + "tags": { + "Name": "testapp-HTTP/S Security Group" + }, + "vpc_id": "\${data.aws_vpc.testPocketApp_pocket_vpc_C4E157E3.id}" + }, + "testPocketApp_ecs_service_ecs_security_group_9C016FA8": { + "description": "Internal ECS Security Group (Managed by Terraform)", + "egress": [ + { + "cidr_blocks": [ + "0.0.0.0/0" + ], + "description": "required", + "from_port": 0, + "ipv6_cidr_blocks": [ + ], + "prefix_list_ids": [ + ], + "protocol": "-1", + "security_groups": [ + ], + "self": null, + "to_port": 0 + } + ], + "ingress": [ + { + "cidr_blocks": [ + ], + "description": "required", + "from_port": 80, + "ipv6_cidr_blocks": [ + ], + "prefix_list_ids": [ + ], + "protocol": "TCP", + "security_groups": [ + "\${aws_security_group.testPocketApp_application_load_balancer_alb_security_group_91A27046.id}" + ], + "self": null, + "to_port": 8675309 + } + ], + "lifecycle": { + "create_before_destroy": true + }, + "name_prefix": "testapp-ECSSecurityGroup", + "vpc_id": "\${data.aws_vpc.testPocketApp_pocket_vpc_C4E157E3.id}" + } + } + } +}" +`; + +exports[`PocketALBApplication renders an application with default autoscaling group and tags 1`] = ` +"{ + "data": { + "aws_caller_identity": { + "testPocketApp_pocket_vpc_current_identity_06D87057": { + } + }, + "aws_iam_policy_document": { + "testPocketApp_ecs_service_ecs-iam_ecs-task-assume_ABB81680": { + "statement": [ + { + "actions": [ + "sts:AssumeRole" + ], + "effect": "Allow", + "principals": [ + { + "identifiers": [ + "ecs-tasks.amazonaws.com" + ], + "type": "Service" + } + ] + } + ], + "version": "2012-10-17" + } + }, + "aws_kms_alias": { + "testPocketApp_pocket_vpc_secrets_manager_key_9FD2BC93": { + "name": "alias/aws/secretsmanager" + } + }, + "aws_region": { + "testPocketApp_pocket_vpc_current_region_8ED435E7": { + } + }, + "aws_route53_zone": { + "testPocketApp_base_dns_main_hosted_zone_9442EEB0": { + "name": "bowling.gov" + } + }, + "aws_security_groups": { + "testPocketApp_pocket_vpc_default_security_groups_40B4FC48": { + "filter": [ + { + "name": "group-name", + "values": [ + "default" + ] + }, + { + "name": "vpc-id", + "values": [ + "\${data.aws_vpc.testPocketApp_pocket_vpc_C4E157E3.id}" + ] + } + ] + }, + "testPocketApp_pocket_vpc_internal_security_groups_12F7F666": { + "filter": [ + { + "name": "group-name", + "values": [ + "pocket-vpc-internal" + ] + }, + { + "name": "vpc-id", + "values": [ + "\${data.aws_vpc.testPocketApp_pocket_vpc_C4E157E3.id}" + ] + } + ] + } + }, + "aws_ssm_parameter": { + "testPocketApp_pocket_vpc_private_subnets_9D449563": { + "name": "/Shared/PrivateSubnets" + }, + "testPocketApp_pocket_vpc_public_subnets_B0B3A6AB": { + "name": "/Shared/PublicSubnets" + }, + "testPocketApp_pocket_vpc_vpc_ssm_param_235525D4": { + "name": "/Shared/Vpc" + } + }, + "aws_subnets": { + "testPocketApp_pocket_vpc_private_subnet_ids_EB1E3A65": { + "filter": [ + { + "name": "subnet-id", + "values": "\${split(\\",\\", data.aws_ssm_parameter.testPocketApp_pocket_vpc_private_subnets_9D449563.value)}" + }, + { + "name": "vpc-id", + "values": [ + "\${data.aws_vpc.testPocketApp_pocket_vpc_C4E157E3.id}" + ] + } + ] + }, + "testPocketApp_pocket_vpc_public_subnet_ids_01F0B902": { + "filter": [ + { + "name": "subnet-id", + "values": "\${split(\\",\\", data.aws_ssm_parameter.testPocketApp_pocket_vpc_public_subnets_B0B3A6AB.value)}" + }, + { + "name": "vpc-id", + "values": [ + "\${data.aws_vpc.testPocketApp_pocket_vpc_C4E157E3.id}" + ] + } + ] + } + }, + "aws_vpc": { + "testPocketApp_pocket_vpc_C4E157E3": { + "filter": [ + { + "name": "vpc-id", + "values": [ + "\${data.aws_ssm_parameter.testPocketApp_pocket_vpc_vpc_ssm_param_235525D4.value}" + ] + } + ] + } + } + }, + "resource": { + "aws_acm_certificate": { + "testPocketApp_alb_certificate_417C14FF": { + "domain_name": "testing.bowling.gov", + "lifecycle": { + "create_before_destroy": true + }, + "tags": { + "hobby": "bowling", + "name": "thedude" + }, + "validation_method": "DNS" + } + }, + "aws_acm_certificate_validation": { + "testPocketApp_alb_certificate_certificate_validation_7D899C6A": { + "certificate_arn": "\${aws_acm_certificate.testPocketApp_alb_certificate_417C14FF.arn}", + "depends_on": [ + "aws_route53_record.testPocketApp_alb_certificate_certificate_record_3C49201F", + "aws_acm_certificate.testPocketApp_alb_certificate_417C14FF" + ], + "validation_record_fqdns": [ + "\${aws_route53_record.testPocketApp_alb_certificate_certificate_record_3C49201F.fqdn}" + ] + } + }, + "aws_alb": { + "testPocketApp_application_load_balancer_alb_D538DEEE": { + "internal": false, + "name_prefix": "TSTAPP", + "security_groups": [ + "\${aws_security_group.testPocketApp_application_load_balancer_alb_security_group_91A27046.id}" + ], + "subnets": "\${data.aws_subnets.testPocketApp_pocket_vpc_public_subnet_ids_01F0B902.ids}", + "tags": { + "hobby": "bowling", + "name": "thedude" + } + } + }, + "aws_alb_listener": { + "testPocketApp_listener_http_A9E227C2": { + "default_action": [ + { + "redirect": { + "port": "443", + "protocol": "HTTPS", + "status_code": "HTTP_301" + }, + "type": "redirect" + } + ], + "load_balancer_arn": "\${aws_alb.testPocketApp_application_load_balancer_alb_D538DEEE.arn}", + "port": 80, + "protocol": "HTTP", + "tags": { + "hobby": "bowling", + "name": "thedude" + } + }, + "testPocketApp_listener_https_7F49DE83": { + "certificate_arn": "\${aws_acm_certificate.testPocketApp_alb_certificate_417C14FF.arn}", + "default_action": [ + { + "fixed_response": { + "content_type": "text/plain", + "message_body": "", + "status_code": "503" + }, + "type": "fixed-response" + } + ], + "load_balancer_arn": "\${aws_alb.testPocketApp_application_load_balancer_alb_D538DEEE.arn}", + "port": 443, + "protocol": "HTTPS", + "ssl_policy": "ELBSecurityPolicy-TLS-1-1-2017-01", + "tags": { + "hobby": "bowling", + "name": "thedude" + } + } + }, + "aws_alb_listener_rule": { + "testPocketApp_ecs_service_listener_rule_C03BB04D": { + "action": [ + { + "target_group_arn": "\${aws_alb_target_group.testPocketApp_ecs_service_blue_target_group_ecs_target_group_A4B85885.arn}", + "type": "forward" + } + ], + "condition": [ + { + "path_pattern": { + "values": [ + "*" + ] + } + } + ], + "lifecycle": { + "ignore_changes": [ + "action" + ] + }, + "listener_arn": "\${aws_alb_listener.testPocketApp_listener_https_7F49DE83.arn}", + "priority": 1, + "tags": { + "hobby": "bowling", + "name": "thedude" + } + } + }, + "aws_alb_target_group": { + "testPocketApp_ecs_service_blue_target_group_ecs_target_group_A4B85885": { + "deregistration_delay": "120", + "health_check": { + "healthy_threshold": 5, + "interval": 15, + "path": "/test", + "protocol": "HTTP", + "unhealthy_threshold": 3 + }, + "name_prefix": "TSTAPP", + "port": 80, + "protocol": "HTTP", + "tags": { + "hobby": "bowling", + "name": "thedude", + "type": "blue" + }, + "target_type": "ip", + "vpc_id": "\${data.aws_vpc.testPocketApp_pocket_vpc_C4E157E3.id}" + } + }, + "aws_appautoscaling_policy": { + "testPocketApp_autoscaling_scale_in_policy_A163A235": { + "depends_on": [ + "aws_appautoscaling_target.testPocketApp_autoscaling_autoscaling_target_505E4EA1" + ], + "name": "testapp-ScaleInPolicy", + "policy_type": "StepScaling", + "resource_id": "\${aws_appautoscaling_target.testPocketApp_autoscaling_autoscaling_target_505E4EA1.resource_id}", + "scalable_dimension": "\${aws_appautoscaling_target.testPocketApp_autoscaling_autoscaling_target_505E4EA1.scalable_dimension}", + "service_namespace": "\${aws_appautoscaling_target.testPocketApp_autoscaling_autoscaling_target_505E4EA1.service_namespace}", + "step_scaling_policy_configuration": { + "adjustment_type": "ChangeInCapacity", + "cooldown": 60, + "metric_aggregation_type": "Average", + "step_adjustment": [ + { + "metric_interval_upper_bound": "0", + "scaling_adjustment": -1 + } + ] + }, + "target_tracking_scaling_policy_configuration": null + }, + "testPocketApp_autoscaling_scale_out_policy_075BFAC4": { + "depends_on": [ + "aws_appautoscaling_target.testPocketApp_autoscaling_autoscaling_target_505E4EA1" + ], + "name": "testapp-ScaleOutPolicy", + "policy_type": "StepScaling", + "resource_id": "\${aws_appautoscaling_target.testPocketApp_autoscaling_autoscaling_target_505E4EA1.resource_id}", + "scalable_dimension": "\${aws_appautoscaling_target.testPocketApp_autoscaling_autoscaling_target_505E4EA1.scalable_dimension}", + "service_namespace": "\${aws_appautoscaling_target.testPocketApp_autoscaling_autoscaling_target_505E4EA1.service_namespace}", + "step_scaling_policy_configuration": { + "adjustment_type": "ChangeInCapacity", + "cooldown": 60, + "metric_aggregation_type": "Average", + "step_adjustment": [ + { + "metric_interval_lower_bound": "0", + "scaling_adjustment": 2 + } + ] + }, + "target_tracking_scaling_policy_configuration": null + } + }, + "aws_appautoscaling_target": { + "testPocketApp_autoscaling_autoscaling_target_505E4EA1": { + "max_capacity": 2, + "min_capacity": 1, + "resource_id": "service/\${aws_ecs_cluster.testPocketApp_ecs_cluster_C3960066.name}/\${aws_ecs_service.testPocketApp_ecs_service_ecs-service_182DEA4C.name}", + "scalable_dimension": "ecs:service:DesiredCount", + "service_namespace": "ecs" + } + }, + "aws_cloudwatch_dashboard": { + "testPocketApp_cloudwatch-dashboard_26E9F70C": { + "dashboard_body": "{\\"widgets\\":[{\\"type\\":\\"metric\\",\\"x\\":0,\\"y\\":0,\\"width\\":12,\\"height\\":6,\\"properties\\":{\\"metrics\\":[[\\"AWS/ApplicationELB\\",\\"HTTPCode_Target_4XX_Count\\",\\"LoadBalancer\\",\\"\${aws_alb.testPocketApp_application_load_balancer_alb_D538DEEE.arn_suffix}\\",{\\"yAxis\\":\\"left\\",\\"color\\":\\"#ff7f0e\\"}],[\\".\\",\\"RequestCount\\",\\".\\",\\".\\",{\\"yAxis\\":\\"right\\",\\"color\\":\\"#1f77b4\\"}],[\\".\\",\\"HTTPCode_Target_5XX_Count\\",\\".\\",\\".\\",{\\"color\\":\\"#d62728\\"}],[\\".\\",\\"HTTPCode_Target_2XX_Count\\",\\".\\",\\".\\",{\\"yAxis\\":\\"right\\",\\"color\\":\\"#2ca02c\\"}]],\\"view\\":\\"timeSeries\\",\\"stacked\\":false,\\"region\\":\\"us-east-1\\",\\"period\\":60,\\"stat\\":\\"Sum\\",\\"title\\":\\"Target Requests\\"}},{\\"type\\":\\"metric\\",\\"x\\":12,\\"y\\":0,\\"width\\":12,\\"height\\":6,\\"properties\\":{\\"metrics\\":[[\\"AWS/ApplicationELB\\",\\"HTTPCode_ELB_4XX_Count\\",\\"LoadBalancer\\",\\"\${aws_alb.testPocketApp_application_load_balancer_alb_D538DEEE.arn_suffix}\\",{\\"yAxis\\":\\"left\\",\\"color\\":\\"#ff7f0e\\"}],[\\".\\",\\"RequestCount\\",\\".\\",\\".\\",{\\"yAxis\\":\\"right\\",\\"color\\":\\"#1f77b4\\"}],[\\".\\",\\"HTTPCode_ELB_5XX_Count\\",\\".\\",\\".\\",{\\"color\\":\\"#d62728\\"}]],\\"view\\":\\"timeSeries\\",\\"stacked\\":false,\\"region\\":\\"us-east-1\\",\\"period\\":60,\\"stat\\":\\"Sum\\",\\"title\\":\\"ALB Requests\\"}},{\\"type\\":\\"metric\\",\\"x\\":12,\\"y\\":6,\\"width\\":12,\\"height\\":6,\\"properties\\":{\\"metrics\\":[[\\"AWS/ApplicationELB\\",\\"TargetResponseTime\\",\\"LoadBalancer\\",\\"\${aws_alb.testPocketApp_application_load_balancer_alb_D538DEEE.arn_suffix}\\",{\\"label\\":\\"Average\\",\\"color\\":\\"#aec7e8\\"}],[\\"...\\",{\\"stat\\":\\"p95\\",\\"label\\":\\"p95\\",\\"color\\":\\"#ffbb78\\"}],[\\"...\\",{\\"stat\\":\\"p99\\",\\"label\\":\\"p99\\",\\"color\\":\\"#98df8a\\"}]],\\"view\\":\\"timeSeries\\",\\"stacked\\":false,\\"region\\":\\"us-east-1\\",\\"stat\\":\\"Average\\",\\"period\\":60}},{\\"type\\":\\"metric\\",\\"x\\":0,\\"y\\":6,\\"width\\":12,\\"height\\":6,\\"properties\\":{\\"metrics\\":[[\\"ECS/ContainerInsights\\",\\"RunningTaskCount\\",\\"ServiceName\\",\\"\${aws_ecs_service.testPocketApp_ecs_service_ecs-service_182DEA4C.name}\\",\\"ClusterName\\",\\"\${aws_ecs_cluster.testPocketApp_ecs_cluster_C3960066.name}\\",{\\"yAxis\\":\\"right\\",\\"color\\":\\"#c49c94\\"}],[\\"AWS/ECS\\",\\"CPUUtilization\\",\\".\\",\\".\\",\\".\\",\\".\\",{\\"color\\":\\"#f7b6d2\\"}],[\\".\\",\\"MemoryUtilization\\",\\".\\",\\".\\",\\".\\",\\".\\",{\\"color\\":\\"#c7c7c7\\"}]],\\"view\\":\\"timeSeries\\",\\"stacked\\":false,\\"region\\":\\"us-east-1\\",\\"stat\\":\\"Average\\",\\"period\\":60,\\"annotations\\":{\\"horizontal\\":[{\\"color\\":\\"#e377c2\\",\\"label\\":\\"CPU scale out\\",\\"value\\":45},{\\"color\\":\\"#c5b0d5\\",\\"label\\":\\"CPU scale in\\",\\"value\\":30}]},\\"title\\":\\"Service Load\\"}}]}", + "dashboard_name": "testapp", + "lifecycle": { + "ignore_changes": [ + "dashboard_body" + ] + } + } + }, + "aws_cloudwatch_metric_alarm": { + "testPocketApp_autoscaling_scale_in_alarm_69B5F00D": { + "alarm_actions": [ + "\${aws_appautoscaling_policy.testPocketApp_autoscaling_scale_in_policy_A163A235.arn}" + ], + "alarm_description": "Alarm to reduce capacity if container CPU is low", + "alarm_name": "testapp Service Low CPU", + "comparison_operator": "LessThanThreshold", + "dimensions": { + "ClusterName": "\${aws_ecs_cluster.testPocketApp_ecs_cluster_C3960066.name}", + "ServiceName": "\${aws_ecs_service.testPocketApp_ecs_service_ecs-service_182DEA4C.name}" + }, + "evaluation_periods": 2, + "metric_name": "CPUUtilization", + "namespace": "AWS/ECS", + "period": 60, + "statistic": "Average", + "tags": { + "hobby": "bowling", + "name": "thedude" + }, + "threshold": 30, + "treat_missing_data": "notBreaching" + }, + "testPocketApp_autoscaling_scale_out_alarm_4313FBE9": { + "alarm_actions": [ + "\${aws_appautoscaling_policy.testPocketApp_autoscaling_scale_out_policy_075BFAC4.arn}" + ], + "alarm_description": "Alarm to add capacity if container CPU is high", + "alarm_name": "testapp Service High CPU", + "comparison_operator": "GreaterThanThreshold", + "dimensions": { + "ClusterName": "\${aws_ecs_cluster.testPocketApp_ecs_cluster_C3960066.name}", + "ServiceName": "\${aws_ecs_service.testPocketApp_ecs_service_ecs-service_182DEA4C.name}" + }, + "evaluation_periods": 2, + "metric_name": "CPUUtilization", + "namespace": "AWS/ECS", + "period": 60, + "statistic": "Average", + "tags": { + "hobby": "bowling", + "name": "thedude" + }, + "threshold": 45, + "treat_missing_data": "notBreaching" + } + }, + "aws_ecs_cluster": { + "testPocketApp_ecs_cluster_C3960066": { + "name": "testapp", + "setting": [ + { + "name": "containerInsights", + "value": "enabled" + } + ], + "tags": { + "hobby": "bowling", + "name": "thedude" + } + } + }, + "aws_ecs_service": { + "testPocketApp_ecs_service_ecs-service_182DEA4C": { + "cluster": "\${aws_ecs_cluster.testPocketApp_ecs_cluster_C3960066.arn}", + "depends_on": [ + "aws_alb_target_group.testPocketApp_ecs_service_blue_target_group_ecs_target_group_A4B85885", + "aws_alb_listener_rule.testPocketApp_ecs_service_listener_rule_C03BB04D" + ], + "deployment_controller": { + "type": "ECS" + }, + "deployment_maximum_percent": 200, + "deployment_minimum_healthy_percent": 100, + "desired_count": 2, + "launch_type": "FARGATE", + "lifecycle": { + "ignore_changes": [ + "desired_count", + "load_balancer" + ] + }, + "load_balancer": [ + { + "container_name": "main_container", + "container_port": 8675309, + "target_group_arn": "\${aws_alb_target_group.testPocketApp_ecs_service_blue_target_group_ecs_target_group_A4B85885.arn}" + } + ], + "name": "testapp", + "network_configuration": { + "security_groups": [ + "\${aws_security_group.testPocketApp_ecs_service_ecs_security_group_9C016FA8.id}" + ], + "subnets": "\${data.aws_subnets.testPocketApp_pocket_vpc_private_subnet_ids_EB1E3A65.ids}" + }, + "propagate_tags": "SERVICE", + "tags": { + "hobby": "bowling", + "name": "thedude" + }, + "task_definition": "\${aws_ecs_task_definition.testPocketApp_ecs_service_ecs-task_A7E74E45.arn}" + } + }, + "aws_ecs_task_definition": { + "testPocketApp_ecs_service_ecs-task_A7E74E45": { + "container_definitions": "[]", + "cpu": "512", + "depends_on": [ + ], + "execution_role_arn": "\${aws_iam_role.testPocketApp_ecs_service_ecs-iam_ecs-execution-role_EB461A0C.arn}", + "family": "testapp", + "memory": "2048", + "network_mode": "awsvpc", + "requires_compatibilities": [ + "FARGATE" + ], + "tags": { + "hobby": "bowling", + "name": "thedude" + }, + "task_role_arn": "\${aws_iam_role.testPocketApp_ecs_service_ecs-iam_ecs-task-role_7DB93963.arn}", + "volume": [ + ] + } + }, + "aws_iam_role": { + "testPocketApp_ecs_service_ecs-iam_ecs-execution-role_EB461A0C": { + "assume_role_policy": "\${data.aws_iam_policy_document.testPocketApp_ecs_service_ecs-iam_ecs-task-assume_ABB81680.json}", + "name": "testapp-TaskExecutionRole", + "tags": { + "hobby": "bowling", + "name": "thedude" + } + }, + "testPocketApp_ecs_service_ecs-iam_ecs-task-role_7DB93963": { + "assume_role_policy": "\${data.aws_iam_policy_document.testPocketApp_ecs_service_ecs-iam_ecs-task-assume_ABB81680.json}", + "name": "testapp-TaskRole", + "tags": { + "hobby": "bowling", + "name": "thedude" + } + } + }, + "aws_route53_record": { + "testPocketApp_alb_certificate_certificate_record_3C49201F": { + "depends_on": [ + "aws_acm_certificate.testPocketApp_alb_certificate_417C14FF" + ], + "name": "\${tolist(aws_acm_certificate.testPocketApp_alb_certificate_417C14FF.domain_validation_options).0.resource_record_name}", + "records": [ + "\${tolist(aws_acm_certificate.testPocketApp_alb_certificate_417C14FF.domain_validation_options).0.resource_record_value}" + ], + "ttl": 60, + "type": "\${tolist(aws_acm_certificate.testPocketApp_alb_certificate_417C14FF.domain_validation_options).0.resource_record_type}", + "zone_id": "\${aws_route53_zone.testPocketApp_base_dns_subhosted_zone_1E1E3243.zone_id}" + }, + "testPocketApp_alb_record_7B56ED33": { + "alias": { + "evaluate_target_health": true, + "name": "\${aws_alb.testPocketApp_application_load_balancer_alb_D538DEEE.dns_name}", + "zone_id": "\${aws_alb.testPocketApp_application_load_balancer_alb_D538DEEE.zone_id}" + }, + "lifecycle": { + "ignore_changes": [ + "weighted_routing_policy[0].weight" + ] + }, + "name": "testing.bowling.gov", + "set_identifier": "1", + "type": "A", + "weighted_routing_policy": { + "weight": 1 + }, + "zone_id": "\${aws_route53_zone.testPocketApp_base_dns_subhosted_zone_1E1E3243.zone_id}" + }, + "testPocketApp_base_dns_subhosted_zone_ns_497FFB83": { + "name": "testing.bowling.gov", + "records": "\${aws_route53_zone.testPocketApp_base_dns_subhosted_zone_1E1E3243.name_servers}", + "ttl": 86400, + "type": "NS", + "zone_id": "\${data.aws_route53_zone.testPocketApp_base_dns_main_hosted_zone_9442EEB0.zone_id}" + } + }, + "aws_route53_zone": { + "testPocketApp_base_dns_subhosted_zone_1E1E3243": { + "name": "testing.bowling.gov", + "tags": { + "hobby": "bowling", + "name": "thedude" + } + } + }, + "aws_security_group": { + "testPocketApp_application_load_balancer_alb_security_group_91A27046": { + "description": "External security group (Managed by Terraform)", + "egress": [ + { + "cidr_blocks": [ + "0.0.0.0/0" + ], + "description": "required", + "from_port": 0, + "ipv6_cidr_blocks": [ + ], + "prefix_list_ids": [ + ], + "protocol": "-1", + "security_groups": [ + ], + "self": null, + "to_port": 0 + } + ], + "ingress": [ + { + "cidr_blocks": [ + "0.0.0.0/0" + ], + "description": null, + "from_port": 443, + "ipv6_cidr_blocks": null, + "prefix_list_ids": null, + "protocol": "TCP", + "security_groups": null, + "self": null, + "to_port": 443 + }, + { + "cidr_blocks": [ + "0.0.0.0/0" + ], + "description": null, + "from_port": 80, + "ipv6_cidr_blocks": null, + "prefix_list_ids": null, + "protocol": "TCP", + "security_groups": null, + "self": null, + "to_port": 80 + } + ], + "lifecycle": { + "create_before_destroy": true + }, + "name_prefix": "testapp-HTTP/S Security Group", + "tags": { + "Name": "testapp-HTTP/S Security Group", + "hobby": "bowling", + "name": "thedude" + }, + "vpc_id": "\${data.aws_vpc.testPocketApp_pocket_vpc_C4E157E3.id}" + }, + "testPocketApp_ecs_service_ecs_security_group_9C016FA8": { + "description": "Internal ECS Security Group (Managed by Terraform)", + "egress": [ + { + "cidr_blocks": [ + "0.0.0.0/0" + ], + "description": "required", + "from_port": 0, + "ipv6_cidr_blocks": [ + ], + "prefix_list_ids": [ + ], + "protocol": "-1", + "security_groups": [ + ], + "self": null, + "to_port": 0 + } + ], + "ingress": [ + { + "cidr_blocks": [ + ], + "description": "required", + "from_port": 80, + "ipv6_cidr_blocks": [ + ], + "prefix_list_ids": [ + ], + "protocol": "TCP", + "security_groups": [ + "\${aws_security_group.testPocketApp_application_load_balancer_alb_security_group_91A27046.id}" + ], + "self": null, + "to_port": 8675309 + } + ], + "lifecycle": { + "create_before_destroy": true + }, + "name_prefix": "testapp-ECSSecurityGroup", + "tags": { + "hobby": "bowling", + "name": "thedude" + }, + "vpc_id": "\${data.aws_vpc.testPocketApp_pocket_vpc_C4E157E3.id}" + } + } + } +}" +`; + +exports[`PocketALBApplication renders an application with minimal config 1`] = ` +"{ + "data": { + "aws_caller_identity": { + "testPocketApp_pocket_vpc_current_identity_06D87057": { + } + }, + "aws_iam_policy_document": { + "testPocketApp_ecs_service_ecs-iam_ecs-task-assume_ABB81680": { + "statement": [ + { + "actions": [ + "sts:AssumeRole" + ], + "effect": "Allow", + "principals": [ + { + "identifiers": [ + "ecs-tasks.amazonaws.com" + ], + "type": "Service" + } + ] + } + ], + "version": "2012-10-17" + } + }, + "aws_kms_alias": { + "testPocketApp_pocket_vpc_secrets_manager_key_9FD2BC93": { + "name": "alias/aws/secretsmanager" + } + }, + "aws_region": { + "testPocketApp_pocket_vpc_current_region_8ED435E7": { + } + }, + "aws_route53_zone": { + "testPocketApp_base_dns_main_hosted_zone_9442EEB0": { + "name": "bowling.gov" + } + }, + "aws_security_groups": { + "testPocketApp_pocket_vpc_default_security_groups_40B4FC48": { + "filter": [ + { + "name": "group-name", + "values": [ + "default" + ] + }, + { + "name": "vpc-id", + "values": [ + "\${data.aws_vpc.testPocketApp_pocket_vpc_C4E157E3.id}" + ] + } + ] + }, + "testPocketApp_pocket_vpc_internal_security_groups_12F7F666": { + "filter": [ + { + "name": "group-name", + "values": [ + "pocket-vpc-internal" + ] + }, + { + "name": "vpc-id", + "values": [ + "\${data.aws_vpc.testPocketApp_pocket_vpc_C4E157E3.id}" + ] + } + ] + } + }, + "aws_ssm_parameter": { + "testPocketApp_pocket_vpc_private_subnets_9D449563": { + "name": "/Shared/PrivateSubnets" + }, + "testPocketApp_pocket_vpc_public_subnets_B0B3A6AB": { + "name": "/Shared/PublicSubnets" + }, + "testPocketApp_pocket_vpc_vpc_ssm_param_235525D4": { + "name": "/Shared/Vpc" + } + }, + "aws_subnets": { + "testPocketApp_pocket_vpc_private_subnet_ids_EB1E3A65": { + "filter": [ + { + "name": "subnet-id", + "values": "\${split(\\",\\", data.aws_ssm_parameter.testPocketApp_pocket_vpc_private_subnets_9D449563.value)}" + }, + { + "name": "vpc-id", + "values": [ + "\${data.aws_vpc.testPocketApp_pocket_vpc_C4E157E3.id}" + ] + } + ] + }, + "testPocketApp_pocket_vpc_public_subnet_ids_01F0B902": { + "filter": [ + { + "name": "subnet-id", + "values": "\${split(\\",\\", data.aws_ssm_parameter.testPocketApp_pocket_vpc_public_subnets_B0B3A6AB.value)}" + }, + { + "name": "vpc-id", + "values": [ + "\${data.aws_vpc.testPocketApp_pocket_vpc_C4E157E3.id}" + ] + } + ] + } + }, + "aws_vpc": { + "testPocketApp_pocket_vpc_C4E157E3": { + "filter": [ + { + "name": "vpc-id", + "values": [ + "\${data.aws_ssm_parameter.testPocketApp_pocket_vpc_vpc_ssm_param_235525D4.value}" + ] + } + ] + } + } + }, + "resource": { + "aws_acm_certificate": { + "testPocketApp_alb_certificate_417C14FF": { + "domain_name": "testing.bowling.gov", + "lifecycle": { + "create_before_destroy": true + }, + "validation_method": "DNS" + } + }, + "aws_acm_certificate_validation": { + "testPocketApp_alb_certificate_certificate_validation_7D899C6A": { + "certificate_arn": "\${aws_acm_certificate.testPocketApp_alb_certificate_417C14FF.arn}", + "depends_on": [ + "aws_route53_record.testPocketApp_alb_certificate_certificate_record_3C49201F", + "aws_acm_certificate.testPocketApp_alb_certificate_417C14FF" + ], + "validation_record_fqdns": [ + "\${aws_route53_record.testPocketApp_alb_certificate_certificate_record_3C49201F.fqdn}" + ] + } + }, + "aws_alb": { + "testPocketApp_application_load_balancer_alb_D538DEEE": { + "internal": false, + "name_prefix": "TSTAPP", + "security_groups": [ + "\${aws_security_group.testPocketApp_application_load_balancer_alb_security_group_91A27046.id}" + ], + "subnets": "\${data.aws_subnets.testPocketApp_pocket_vpc_public_subnet_ids_01F0B902.ids}" + } + }, + "aws_alb_listener": { + "testPocketApp_listener_http_A9E227C2": { + "default_action": [ + { + "redirect": { + "port": "443", + "protocol": "HTTPS", + "status_code": "HTTP_301" + }, + "type": "redirect" + } + ], + "load_balancer_arn": "\${aws_alb.testPocketApp_application_load_balancer_alb_D538DEEE.arn}", + "port": 80, + "protocol": "HTTP" + }, + "testPocketApp_listener_https_7F49DE83": { + "certificate_arn": "\${aws_acm_certificate.testPocketApp_alb_certificate_417C14FF.arn}", + "default_action": [ + { + "fixed_response": { + "content_type": "text/plain", + "message_body": "", + "status_code": "503" + }, + "type": "fixed-response" + } + ], + "load_balancer_arn": "\${aws_alb.testPocketApp_application_load_balancer_alb_D538DEEE.arn}", + "port": 443, + "protocol": "HTTPS", + "ssl_policy": "ELBSecurityPolicy-TLS-1-1-2017-01" + } + }, + "aws_alb_listener_rule": { + "testPocketApp_ecs_service_listener_rule_C03BB04D": { + "action": [ + { + "target_group_arn": "\${aws_alb_target_group.testPocketApp_ecs_service_blue_target_group_ecs_target_group_A4B85885.arn}", + "type": "forward" + } + ], + "condition": [ + { + "path_pattern": { + "values": [ + "*" + ] + } + } + ], + "lifecycle": { + "ignore_changes": [ + "action" + ] + }, + "listener_arn": "\${aws_alb_listener.testPocketApp_listener_https_7F49DE83.arn}", + "priority": 1 + } + }, + "aws_alb_target_group": { + "testPocketApp_ecs_service_blue_target_group_ecs_target_group_A4B85885": { + "deregistration_delay": "120", + "health_check": { + "healthy_threshold": 5, + "interval": 15, + "path": "/test", + "protocol": "HTTP", + "unhealthy_threshold": 3 + }, + "name_prefix": "TSTAPP", + "port": 80, + "protocol": "HTTP", + "tags": { + "type": "blue" + }, + "target_type": "ip", + "vpc_id": "\${data.aws_vpc.testPocketApp_pocket_vpc_C4E157E3.id}" + } + }, + "aws_appautoscaling_policy": { + "testPocketApp_autoscaling_scale_in_policy_A163A235": { + "depends_on": [ + "aws_appautoscaling_target.testPocketApp_autoscaling_autoscaling_target_505E4EA1" + ], + "name": "testapp-ScaleInPolicy", + "policy_type": "StepScaling", + "resource_id": "\${aws_appautoscaling_target.testPocketApp_autoscaling_autoscaling_target_505E4EA1.resource_id}", + "scalable_dimension": "\${aws_appautoscaling_target.testPocketApp_autoscaling_autoscaling_target_505E4EA1.scalable_dimension}", + "service_namespace": "\${aws_appautoscaling_target.testPocketApp_autoscaling_autoscaling_target_505E4EA1.service_namespace}", + "step_scaling_policy_configuration": { + "adjustment_type": "ChangeInCapacity", + "cooldown": 60, + "metric_aggregation_type": "Average", + "step_adjustment": [ + { + "metric_interval_upper_bound": "0", + "scaling_adjustment": -1 + } + ] + }, + "target_tracking_scaling_policy_configuration": null + }, + "testPocketApp_autoscaling_scale_out_policy_075BFAC4": { + "depends_on": [ + "aws_appautoscaling_target.testPocketApp_autoscaling_autoscaling_target_505E4EA1" + ], + "name": "testapp-ScaleOutPolicy", + "policy_type": "StepScaling", + "resource_id": "\${aws_appautoscaling_target.testPocketApp_autoscaling_autoscaling_target_505E4EA1.resource_id}", + "scalable_dimension": "\${aws_appautoscaling_target.testPocketApp_autoscaling_autoscaling_target_505E4EA1.scalable_dimension}", + "service_namespace": "\${aws_appautoscaling_target.testPocketApp_autoscaling_autoscaling_target_505E4EA1.service_namespace}", + "step_scaling_policy_configuration": { + "adjustment_type": "ChangeInCapacity", + "cooldown": 60, + "metric_aggregation_type": "Average", + "step_adjustment": [ + { + "metric_interval_lower_bound": "0", + "scaling_adjustment": 2 + } + ] + }, + "target_tracking_scaling_policy_configuration": null + } + }, + "aws_appautoscaling_target": { + "testPocketApp_autoscaling_autoscaling_target_505E4EA1": { + "max_capacity": 2, + "min_capacity": 1, + "resource_id": "service/\${aws_ecs_cluster.testPocketApp_ecs_cluster_C3960066.name}/\${aws_ecs_service.testPocketApp_ecs_service_ecs-service_182DEA4C.name}", + "scalable_dimension": "ecs:service:DesiredCount", + "service_namespace": "ecs" + } + }, + "aws_cloudwatch_dashboard": { + "testPocketApp_cloudwatch-dashboard_26E9F70C": { + "dashboard_body": "{\\"widgets\\":[{\\"type\\":\\"metric\\",\\"x\\":0,\\"y\\":0,\\"width\\":12,\\"height\\":6,\\"properties\\":{\\"metrics\\":[[\\"AWS/ApplicationELB\\",\\"HTTPCode_Target_4XX_Count\\",\\"LoadBalancer\\",\\"\${aws_alb.testPocketApp_application_load_balancer_alb_D538DEEE.arn_suffix}\\",{\\"yAxis\\":\\"left\\",\\"color\\":\\"#ff7f0e\\"}],[\\".\\",\\"RequestCount\\",\\".\\",\\".\\",{\\"yAxis\\":\\"right\\",\\"color\\":\\"#1f77b4\\"}],[\\".\\",\\"HTTPCode_Target_5XX_Count\\",\\".\\",\\".\\",{\\"color\\":\\"#d62728\\"}],[\\".\\",\\"HTTPCode_Target_2XX_Count\\",\\".\\",\\".\\",{\\"yAxis\\":\\"right\\",\\"color\\":\\"#2ca02c\\"}]],\\"view\\":\\"timeSeries\\",\\"stacked\\":false,\\"region\\":\\"us-east-1\\",\\"period\\":60,\\"stat\\":\\"Sum\\",\\"title\\":\\"Target Requests\\"}},{\\"type\\":\\"metric\\",\\"x\\":12,\\"y\\":0,\\"width\\":12,\\"height\\":6,\\"properties\\":{\\"metrics\\":[[\\"AWS/ApplicationELB\\",\\"HTTPCode_ELB_4XX_Count\\",\\"LoadBalancer\\",\\"\${aws_alb.testPocketApp_application_load_balancer_alb_D538DEEE.arn_suffix}\\",{\\"yAxis\\":\\"left\\",\\"color\\":\\"#ff7f0e\\"}],[\\".\\",\\"RequestCount\\",\\".\\",\\".\\",{\\"yAxis\\":\\"right\\",\\"color\\":\\"#1f77b4\\"}],[\\".\\",\\"HTTPCode_ELB_5XX_Count\\",\\".\\",\\".\\",{\\"color\\":\\"#d62728\\"}]],\\"view\\":\\"timeSeries\\",\\"stacked\\":false,\\"region\\":\\"us-east-1\\",\\"period\\":60,\\"stat\\":\\"Sum\\",\\"title\\":\\"ALB Requests\\"}},{\\"type\\":\\"metric\\",\\"x\\":12,\\"y\\":6,\\"width\\":12,\\"height\\":6,\\"properties\\":{\\"metrics\\":[[\\"AWS/ApplicationELB\\",\\"TargetResponseTime\\",\\"LoadBalancer\\",\\"\${aws_alb.testPocketApp_application_load_balancer_alb_D538DEEE.arn_suffix}\\",{\\"label\\":\\"Average\\",\\"color\\":\\"#aec7e8\\"}],[\\"...\\",{\\"stat\\":\\"p95\\",\\"label\\":\\"p95\\",\\"color\\":\\"#ffbb78\\"}],[\\"...\\",{\\"stat\\":\\"p99\\",\\"label\\":\\"p99\\",\\"color\\":\\"#98df8a\\"}]],\\"view\\":\\"timeSeries\\",\\"stacked\\":false,\\"region\\":\\"us-east-1\\",\\"stat\\":\\"Average\\",\\"period\\":60}},{\\"type\\":\\"metric\\",\\"x\\":0,\\"y\\":6,\\"width\\":12,\\"height\\":6,\\"properties\\":{\\"metrics\\":[[\\"ECS/ContainerInsights\\",\\"RunningTaskCount\\",\\"ServiceName\\",\\"\${aws_ecs_service.testPocketApp_ecs_service_ecs-service_182DEA4C.name}\\",\\"ClusterName\\",\\"\${aws_ecs_cluster.testPocketApp_ecs_cluster_C3960066.name}\\",{\\"yAxis\\":\\"right\\",\\"color\\":\\"#c49c94\\"}],[\\"AWS/ECS\\",\\"CPUUtilization\\",\\".\\",\\".\\",\\".\\",\\".\\",{\\"color\\":\\"#f7b6d2\\"}],[\\".\\",\\"MemoryUtilization\\",\\".\\",\\".\\",\\".\\",\\".\\",{\\"color\\":\\"#c7c7c7\\"}]],\\"view\\":\\"timeSeries\\",\\"stacked\\":false,\\"region\\":\\"us-east-1\\",\\"stat\\":\\"Average\\",\\"period\\":60,\\"annotations\\":{\\"horizontal\\":[{\\"color\\":\\"#e377c2\\",\\"label\\":\\"CPU scale out\\",\\"value\\":45},{\\"color\\":\\"#c5b0d5\\",\\"label\\":\\"CPU scale in\\",\\"value\\":30}]},\\"title\\":\\"Service Load\\"}}]}", + "dashboard_name": "testapp", + "lifecycle": { + "ignore_changes": [ + "dashboard_body" + ] + } + } + }, + "aws_cloudwatch_metric_alarm": { + "testPocketApp_autoscaling_scale_in_alarm_69B5F00D": { + "alarm_actions": [ + "\${aws_appautoscaling_policy.testPocketApp_autoscaling_scale_in_policy_A163A235.arn}" + ], + "alarm_description": "Alarm to reduce capacity if container CPU is low", + "alarm_name": "testapp Service Low CPU", + "comparison_operator": "LessThanThreshold", + "dimensions": { + "ClusterName": "\${aws_ecs_cluster.testPocketApp_ecs_cluster_C3960066.name}", + "ServiceName": "\${aws_ecs_service.testPocketApp_ecs_service_ecs-service_182DEA4C.name}" + }, + "evaluation_periods": 2, + "metric_name": "CPUUtilization", + "namespace": "AWS/ECS", + "period": 60, + "statistic": "Average", + "threshold": 30, + "treat_missing_data": "notBreaching" + }, + "testPocketApp_autoscaling_scale_out_alarm_4313FBE9": { + "alarm_actions": [ + "\${aws_appautoscaling_policy.testPocketApp_autoscaling_scale_out_policy_075BFAC4.arn}" + ], + "alarm_description": "Alarm to add capacity if container CPU is high", + "alarm_name": "testapp Service High CPU", + "comparison_operator": "GreaterThanThreshold", + "dimensions": { + "ClusterName": "\${aws_ecs_cluster.testPocketApp_ecs_cluster_C3960066.name}", + "ServiceName": "\${aws_ecs_service.testPocketApp_ecs_service_ecs-service_182DEA4C.name}" + }, + "evaluation_periods": 2, + "metric_name": "CPUUtilization", + "namespace": "AWS/ECS", + "period": 60, + "statistic": "Average", + "threshold": 45, + "treat_missing_data": "notBreaching" + } + }, + "aws_ecs_cluster": { + "testPocketApp_ecs_cluster_C3960066": { + "name": "testapp", + "setting": [ + { + "name": "containerInsights", + "value": "enabled" + } + ] + } + }, + "aws_ecs_service": { + "testPocketApp_ecs_service_ecs-service_182DEA4C": { + "cluster": "\${aws_ecs_cluster.testPocketApp_ecs_cluster_C3960066.arn}", + "depends_on": [ + "aws_alb_target_group.testPocketApp_ecs_service_blue_target_group_ecs_target_group_A4B85885", + "aws_alb_listener_rule.testPocketApp_ecs_service_listener_rule_C03BB04D" + ], + "deployment_controller": { + "type": "ECS" + }, + "deployment_maximum_percent": 200, + "deployment_minimum_healthy_percent": 100, + "desired_count": 2, + "launch_type": "FARGATE", + "lifecycle": { + "ignore_changes": [ + "desired_count", + "load_balancer" + ] + }, + "load_balancer": [ + { + "container_name": "main_container", + "container_port": 8675309, + "target_group_arn": "\${aws_alb_target_group.testPocketApp_ecs_service_blue_target_group_ecs_target_group_A4B85885.arn}" + } + ], + "name": "testapp", + "network_configuration": { + "security_groups": [ + "\${aws_security_group.testPocketApp_ecs_service_ecs_security_group_9C016FA8.id}" + ], + "subnets": "\${data.aws_subnets.testPocketApp_pocket_vpc_private_subnet_ids_EB1E3A65.ids}" + }, + "propagate_tags": "SERVICE", + "task_definition": "\${aws_ecs_task_definition.testPocketApp_ecs_service_ecs-task_A7E74E45.arn}" + } + }, + "aws_ecs_task_definition": { + "testPocketApp_ecs_service_ecs-task_A7E74E45": { + "container_definitions": "[]", + "cpu": "512", + "depends_on": [ + ], + "execution_role_arn": "\${aws_iam_role.testPocketApp_ecs_service_ecs-iam_ecs-execution-role_EB461A0C.arn}", + "family": "testapp", + "memory": "2048", + "network_mode": "awsvpc", + "requires_compatibilities": [ + "FARGATE" + ], + "task_role_arn": "\${aws_iam_role.testPocketApp_ecs_service_ecs-iam_ecs-task-role_7DB93963.arn}", + "volume": [ + ] + } + }, + "aws_iam_role": { + "testPocketApp_ecs_service_ecs-iam_ecs-execution-role_EB461A0C": { + "assume_role_policy": "\${data.aws_iam_policy_document.testPocketApp_ecs_service_ecs-iam_ecs-task-assume_ABB81680.json}", + "name": "testapp-TaskExecutionRole" + }, + "testPocketApp_ecs_service_ecs-iam_ecs-task-role_7DB93963": { + "assume_role_policy": "\${data.aws_iam_policy_document.testPocketApp_ecs_service_ecs-iam_ecs-task-assume_ABB81680.json}", + "name": "testapp-TaskRole" + } + }, + "aws_route53_record": { + "testPocketApp_alb_certificate_certificate_record_3C49201F": { + "depends_on": [ + "aws_acm_certificate.testPocketApp_alb_certificate_417C14FF" + ], + "name": "\${tolist(aws_acm_certificate.testPocketApp_alb_certificate_417C14FF.domain_validation_options).0.resource_record_name}", + "records": [ + "\${tolist(aws_acm_certificate.testPocketApp_alb_certificate_417C14FF.domain_validation_options).0.resource_record_value}" + ], + "ttl": 60, + "type": "\${tolist(aws_acm_certificate.testPocketApp_alb_certificate_417C14FF.domain_validation_options).0.resource_record_type}", + "zone_id": "\${aws_route53_zone.testPocketApp_base_dns_subhosted_zone_1E1E3243.zone_id}" + }, + "testPocketApp_alb_record_7B56ED33": { + "alias": { + "evaluate_target_health": true, + "name": "\${aws_alb.testPocketApp_application_load_balancer_alb_D538DEEE.dns_name}", + "zone_id": "\${aws_alb.testPocketApp_application_load_balancer_alb_D538DEEE.zone_id}" + }, + "lifecycle": { + "ignore_changes": [ + "weighted_routing_policy[0].weight" + ] + }, + "name": "testing.bowling.gov", + "set_identifier": "1", + "type": "A", + "weighted_routing_policy": { + "weight": 1 + }, + "zone_id": "\${aws_route53_zone.testPocketApp_base_dns_subhosted_zone_1E1E3243.zone_id}" + }, + "testPocketApp_base_dns_subhosted_zone_ns_497FFB83": { + "name": "testing.bowling.gov", + "records": "\${aws_route53_zone.testPocketApp_base_dns_subhosted_zone_1E1E3243.name_servers}", + "ttl": 86400, + "type": "NS", + "zone_id": "\${data.aws_route53_zone.testPocketApp_base_dns_main_hosted_zone_9442EEB0.zone_id}" + } + }, + "aws_route53_zone": { + "testPocketApp_base_dns_subhosted_zone_1E1E3243": { + "name": "testing.bowling.gov" + } + }, + "aws_security_group": { + "testPocketApp_application_load_balancer_alb_security_group_91A27046": { + "description": "External security group (Managed by Terraform)", + "egress": [ + { + "cidr_blocks": [ + "0.0.0.0/0" + ], + "description": "required", + "from_port": 0, + "ipv6_cidr_blocks": [ + ], + "prefix_list_ids": [ + ], + "protocol": "-1", + "security_groups": [ + ], + "self": null, + "to_port": 0 + } + ], + "ingress": [ + { + "cidr_blocks": [ + "0.0.0.0/0" + ], + "description": null, + "from_port": 443, + "ipv6_cidr_blocks": null, + "prefix_list_ids": null, + "protocol": "TCP", + "security_groups": null, + "self": null, + "to_port": 443 + }, + { + "cidr_blocks": [ + "0.0.0.0/0" + ], + "description": null, + "from_port": 80, + "ipv6_cidr_blocks": null, + "prefix_list_ids": null, + "protocol": "TCP", + "security_groups": null, + "self": null, + "to_port": 80 + } + ], + "lifecycle": { + "create_before_destroy": true + }, + "name_prefix": "testapp-HTTP/S Security Group", + "tags": { + "Name": "testapp-HTTP/S Security Group" + }, + "vpc_id": "\${data.aws_vpc.testPocketApp_pocket_vpc_C4E157E3.id}" + }, + "testPocketApp_ecs_service_ecs_security_group_9C016FA8": { + "description": "Internal ECS Security Group (Managed by Terraform)", + "egress": [ + { + "cidr_blocks": [ + "0.0.0.0/0" + ], + "description": "required", + "from_port": 0, + "ipv6_cidr_blocks": [ + ], + "prefix_list_ids": [ + ], + "protocol": "-1", + "security_groups": [ + ], + "self": null, + "to_port": 0 + } + ], + "ingress": [ + { + "cidr_blocks": [ + ], + "description": "required", + "from_port": 80, + "ipv6_cidr_blocks": [ + ], + "prefix_list_ids": [ + ], + "protocol": "TCP", + "security_groups": [ + "\${aws_security_group.testPocketApp_application_load_balancer_alb_security_group_91A27046.id}" + ], + "self": null, + "to_port": 8675309 + } + ], + "lifecycle": { + "create_before_destroy": true + }, + "name_prefix": "testapp-ECSSecurityGroup", + "vpc_id": "\${data.aws_vpc.testPocketApp_pocket_vpc_C4E157E3.id}" + } + } + } +}" +`; + +exports[`PocketALBApplication renders an application with modified container def protocol, cpu and memory reservation 1`] = ` +"{ + "data": { + "aws_caller_identity": { + "testPocketApp_pocket_vpc_current_identity_06D87057": { + } + }, + "aws_iam_policy_document": { + "testPocketApp_ecs_service_ecs-iam_ecs-task-assume_ABB81680": { + "statement": [ + { + "actions": [ + "sts:AssumeRole" + ], + "effect": "Allow", + "principals": [ + { + "identifiers": [ + "ecs-tasks.amazonaws.com" + ], + "type": "Service" + } + ] + } + ], + "version": "2012-10-17" + } + }, + "aws_kms_alias": { + "testPocketApp_pocket_vpc_secrets_manager_key_9FD2BC93": { + "name": "alias/aws/secretsmanager" + } + }, + "aws_region": { + "testPocketApp_pocket_vpc_current_region_8ED435E7": { + } + }, + "aws_route53_zone": { + "testPocketApp_base_dns_main_hosted_zone_9442EEB0": { + "name": "bowling.gov" + } + }, + "aws_security_groups": { + "testPocketApp_pocket_vpc_default_security_groups_40B4FC48": { + "filter": [ + { + "name": "group-name", + "values": [ + "default" + ] + }, + { + "name": "vpc-id", + "values": [ + "\${data.aws_vpc.testPocketApp_pocket_vpc_C4E157E3.id}" + ] + } + ] + }, + "testPocketApp_pocket_vpc_internal_security_groups_12F7F666": { + "filter": [ + { + "name": "group-name", + "values": [ + "pocket-vpc-internal" + ] + }, + { + "name": "vpc-id", + "values": [ + "\${data.aws_vpc.testPocketApp_pocket_vpc_C4E157E3.id}" + ] + } + ] + } + }, + "aws_ssm_parameter": { + "testPocketApp_pocket_vpc_private_subnets_9D449563": { + "name": "/Shared/PrivateSubnets" + }, + "testPocketApp_pocket_vpc_public_subnets_B0B3A6AB": { + "name": "/Shared/PublicSubnets" + }, + "testPocketApp_pocket_vpc_vpc_ssm_param_235525D4": { + "name": "/Shared/Vpc" + } + }, + "aws_subnets": { + "testPocketApp_pocket_vpc_private_subnet_ids_EB1E3A65": { + "filter": [ + { + "name": "subnet-id", + "values": "\${split(\\",\\", data.aws_ssm_parameter.testPocketApp_pocket_vpc_private_subnets_9D449563.value)}" + }, + { + "name": "vpc-id", + "values": [ + "\${data.aws_vpc.testPocketApp_pocket_vpc_C4E157E3.id}" + ] + } + ] + }, + "testPocketApp_pocket_vpc_public_subnet_ids_01F0B902": { + "filter": [ + { + "name": "subnet-id", + "values": "\${split(\\",\\", data.aws_ssm_parameter.testPocketApp_pocket_vpc_public_subnets_B0B3A6AB.value)}" + }, + { + "name": "vpc-id", + "values": [ + "\${data.aws_vpc.testPocketApp_pocket_vpc_C4E157E3.id}" + ] + } + ] + } + }, + "aws_vpc": { + "testPocketApp_pocket_vpc_C4E157E3": { + "filter": [ + { + "name": "vpc-id", + "values": [ + "\${data.aws_ssm_parameter.testPocketApp_pocket_vpc_vpc_ssm_param_235525D4.value}" + ] + } + ] + } + } + }, + "resource": { + "aws_acm_certificate": { + "testPocketApp_alb_certificate_417C14FF": { + "domain_name": "testing.bowling.gov", + "lifecycle": { + "create_before_destroy": true + }, + "validation_method": "DNS" + } + }, + "aws_acm_certificate_validation": { + "testPocketApp_alb_certificate_certificate_validation_7D899C6A": { + "certificate_arn": "\${aws_acm_certificate.testPocketApp_alb_certificate_417C14FF.arn}", + "depends_on": [ + "aws_route53_record.testPocketApp_alb_certificate_certificate_record_3C49201F", + "aws_acm_certificate.testPocketApp_alb_certificate_417C14FF" + ], + "validation_record_fqdns": [ + "\${aws_route53_record.testPocketApp_alb_certificate_certificate_record_3C49201F.fqdn}" + ] + } + }, + "aws_alb": { + "testPocketApp_application_load_balancer_alb_D538DEEE": { + "internal": false, + "name_prefix": "TSTAPP", + "security_groups": [ + "\${aws_security_group.testPocketApp_application_load_balancer_alb_security_group_91A27046.id}" + ], + "subnets": "\${data.aws_subnets.testPocketApp_pocket_vpc_public_subnet_ids_01F0B902.ids}" + } + }, + "aws_alb_listener": { + "testPocketApp_listener_http_A9E227C2": { + "default_action": [ + { + "redirect": { + "port": "443", + "protocol": "HTTPS", + "status_code": "HTTP_301" + }, + "type": "redirect" + } + ], + "load_balancer_arn": "\${aws_alb.testPocketApp_application_load_balancer_alb_D538DEEE.arn}", + "port": 80, + "protocol": "HTTP" + }, + "testPocketApp_listener_https_7F49DE83": { + "certificate_arn": "\${aws_acm_certificate.testPocketApp_alb_certificate_417C14FF.arn}", + "default_action": [ + { + "fixed_response": { + "content_type": "text/plain", + "message_body": "", + "status_code": "503" + }, + "type": "fixed-response" + } + ], + "load_balancer_arn": "\${aws_alb.testPocketApp_application_load_balancer_alb_D538DEEE.arn}", + "port": 443, + "protocol": "HTTPS", + "ssl_policy": "ELBSecurityPolicy-TLS-1-1-2017-01" + } + }, + "aws_alb_listener_rule": { + "testPocketApp_ecs_service_listener_rule_C03BB04D": { + "action": [ + { + "target_group_arn": "\${aws_alb_target_group.testPocketApp_ecs_service_blue_target_group_ecs_target_group_A4B85885.arn}", + "type": "forward" + } + ], + "condition": [ + { + "path_pattern": { + "values": [ + "*" + ] + } + } + ], + "lifecycle": { + "ignore_changes": [ + "action" + ] + }, + "listener_arn": "\${aws_alb_listener.testPocketApp_listener_https_7F49DE83.arn}", + "priority": 1 + } + }, + "aws_alb_target_group": { + "testPocketApp_ecs_service_blue_target_group_ecs_target_group_A4B85885": { + "deregistration_delay": "120", + "health_check": { + "healthy_threshold": 5, + "interval": 15, + "path": "/test", + "protocol": "HTTP", + "unhealthy_threshold": 3 + }, + "name_prefix": "TSTAPP", + "port": 80, + "protocol": "HTTP", + "tags": { + "type": "blue" + }, + "target_type": "ip", + "vpc_id": "\${data.aws_vpc.testPocketApp_pocket_vpc_C4E157E3.id}" + } + }, + "aws_appautoscaling_policy": { + "testPocketApp_autoscaling_scale_in_policy_A163A235": { + "depends_on": [ + "aws_appautoscaling_target.testPocketApp_autoscaling_autoscaling_target_505E4EA1" + ], + "name": "testapp-ScaleInPolicy", + "policy_type": "StepScaling", + "resource_id": "\${aws_appautoscaling_target.testPocketApp_autoscaling_autoscaling_target_505E4EA1.resource_id}", + "scalable_dimension": "\${aws_appautoscaling_target.testPocketApp_autoscaling_autoscaling_target_505E4EA1.scalable_dimension}", + "service_namespace": "\${aws_appautoscaling_target.testPocketApp_autoscaling_autoscaling_target_505E4EA1.service_namespace}", + "step_scaling_policy_configuration": { + "adjustment_type": "ChangeInCapacity", + "cooldown": 60, + "metric_aggregation_type": "Average", + "step_adjustment": [ + { + "metric_interval_upper_bound": "0", + "scaling_adjustment": -1 + } + ] + }, + "target_tracking_scaling_policy_configuration": null + }, + "testPocketApp_autoscaling_scale_out_policy_075BFAC4": { + "depends_on": [ + "aws_appautoscaling_target.testPocketApp_autoscaling_autoscaling_target_505E4EA1" + ], + "name": "testapp-ScaleOutPolicy", + "policy_type": "StepScaling", + "resource_id": "\${aws_appautoscaling_target.testPocketApp_autoscaling_autoscaling_target_505E4EA1.resource_id}", + "scalable_dimension": "\${aws_appautoscaling_target.testPocketApp_autoscaling_autoscaling_target_505E4EA1.scalable_dimension}", + "service_namespace": "\${aws_appautoscaling_target.testPocketApp_autoscaling_autoscaling_target_505E4EA1.service_namespace}", + "step_scaling_policy_configuration": { + "adjustment_type": "ChangeInCapacity", + "cooldown": 60, + "metric_aggregation_type": "Average", + "step_adjustment": [ + { + "metric_interval_lower_bound": "0", + "scaling_adjustment": 2 + } + ] + }, + "target_tracking_scaling_policy_configuration": null + } + }, + "aws_appautoscaling_target": { + "testPocketApp_autoscaling_autoscaling_target_505E4EA1": { + "max_capacity": 2, + "min_capacity": 1, + "resource_id": "service/\${aws_ecs_cluster.testPocketApp_ecs_cluster_C3960066.name}/\${aws_ecs_service.testPocketApp_ecs_service_ecs-service_182DEA4C.name}", + "scalable_dimension": "ecs:service:DesiredCount", + "service_namespace": "ecs" + } + }, + "aws_cloudwatch_dashboard": { + "testPocketApp_cloudwatch-dashboard_26E9F70C": { + "dashboard_body": "{\\"widgets\\":[{\\"type\\":\\"metric\\",\\"x\\":0,\\"y\\":0,\\"width\\":12,\\"height\\":6,\\"properties\\":{\\"metrics\\":[[\\"AWS/ApplicationELB\\",\\"HTTPCode_Target_4XX_Count\\",\\"LoadBalancer\\",\\"\${aws_alb.testPocketApp_application_load_balancer_alb_D538DEEE.arn_suffix}\\",{\\"yAxis\\":\\"left\\",\\"color\\":\\"#ff7f0e\\"}],[\\".\\",\\"RequestCount\\",\\".\\",\\".\\",{\\"yAxis\\":\\"right\\",\\"color\\":\\"#1f77b4\\"}],[\\".\\",\\"HTTPCode_Target_5XX_Count\\",\\".\\",\\".\\",{\\"color\\":\\"#d62728\\"}],[\\".\\",\\"HTTPCode_Target_2XX_Count\\",\\".\\",\\".\\",{\\"yAxis\\":\\"right\\",\\"color\\":\\"#2ca02c\\"}]],\\"view\\":\\"timeSeries\\",\\"stacked\\":false,\\"region\\":\\"us-east-1\\",\\"period\\":60,\\"stat\\":\\"Sum\\",\\"title\\":\\"Target Requests\\"}},{\\"type\\":\\"metric\\",\\"x\\":12,\\"y\\":0,\\"width\\":12,\\"height\\":6,\\"properties\\":{\\"metrics\\":[[\\"AWS/ApplicationELB\\",\\"HTTPCode_ELB_4XX_Count\\",\\"LoadBalancer\\",\\"\${aws_alb.testPocketApp_application_load_balancer_alb_D538DEEE.arn_suffix}\\",{\\"yAxis\\":\\"left\\",\\"color\\":\\"#ff7f0e\\"}],[\\".\\",\\"RequestCount\\",\\".\\",\\".\\",{\\"yAxis\\":\\"right\\",\\"color\\":\\"#1f77b4\\"}],[\\".\\",\\"HTTPCode_ELB_5XX_Count\\",\\".\\",\\".\\",{\\"color\\":\\"#d62728\\"}]],\\"view\\":\\"timeSeries\\",\\"stacked\\":false,\\"region\\":\\"us-east-1\\",\\"period\\":60,\\"stat\\":\\"Sum\\",\\"title\\":\\"ALB Requests\\"}},{\\"type\\":\\"metric\\",\\"x\\":12,\\"y\\":6,\\"width\\":12,\\"height\\":6,\\"properties\\":{\\"metrics\\":[[\\"AWS/ApplicationELB\\",\\"TargetResponseTime\\",\\"LoadBalancer\\",\\"\${aws_alb.testPocketApp_application_load_balancer_alb_D538DEEE.arn_suffix}\\",{\\"label\\":\\"Average\\",\\"color\\":\\"#aec7e8\\"}],[\\"...\\",{\\"stat\\":\\"p95\\",\\"label\\":\\"p95\\",\\"color\\":\\"#ffbb78\\"}],[\\"...\\",{\\"stat\\":\\"p99\\",\\"label\\":\\"p99\\",\\"color\\":\\"#98df8a\\"}]],\\"view\\":\\"timeSeries\\",\\"stacked\\":false,\\"region\\":\\"us-east-1\\",\\"stat\\":\\"Average\\",\\"period\\":60}},{\\"type\\":\\"metric\\",\\"x\\":0,\\"y\\":6,\\"width\\":12,\\"height\\":6,\\"properties\\":{\\"metrics\\":[[\\"ECS/ContainerInsights\\",\\"RunningTaskCount\\",\\"ServiceName\\",\\"\${aws_ecs_service.testPocketApp_ecs_service_ecs-service_182DEA4C.name}\\",\\"ClusterName\\",\\"\${aws_ecs_cluster.testPocketApp_ecs_cluster_C3960066.name}\\",{\\"yAxis\\":\\"right\\",\\"color\\":\\"#c49c94\\"}],[\\"AWS/ECS\\",\\"CPUUtilization\\",\\".\\",\\".\\",\\".\\",\\".\\",{\\"color\\":\\"#f7b6d2\\"}],[\\".\\",\\"MemoryUtilization\\",\\".\\",\\".\\",\\".\\",\\".\\",{\\"color\\":\\"#c7c7c7\\"}]],\\"view\\":\\"timeSeries\\",\\"stacked\\":false,\\"region\\":\\"us-east-1\\",\\"stat\\":\\"Average\\",\\"period\\":60,\\"annotations\\":{\\"horizontal\\":[{\\"color\\":\\"#e377c2\\",\\"label\\":\\"CPU scale out\\",\\"value\\":45},{\\"color\\":\\"#c5b0d5\\",\\"label\\":\\"CPU scale in\\",\\"value\\":30}]},\\"title\\":\\"Service Load\\"}}]}", + "dashboard_name": "testapp", + "lifecycle": { + "ignore_changes": [ + "dashboard_body" + ] + } + } + }, + "aws_cloudwatch_log_group": { + "testPocketApp_ecs_service_ecs-xray-daemon_55253A74": { + "name_prefix": "/ecs/testapp/xray-daemon", + "retention_in_days": 30 + } + }, + "aws_cloudwatch_metric_alarm": { + "testPocketApp_autoscaling_scale_in_alarm_69B5F00D": { + "alarm_actions": [ + "\${aws_appautoscaling_policy.testPocketApp_autoscaling_scale_in_policy_A163A235.arn}" + ], + "alarm_description": "Alarm to reduce capacity if container CPU is low", + "alarm_name": "testapp Service Low CPU", + "comparison_operator": "LessThanThreshold", + "dimensions": { + "ClusterName": "\${aws_ecs_cluster.testPocketApp_ecs_cluster_C3960066.name}", + "ServiceName": "\${aws_ecs_service.testPocketApp_ecs_service_ecs-service_182DEA4C.name}" + }, + "evaluation_periods": 2, + "metric_name": "CPUUtilization", + "namespace": "AWS/ECS", + "period": 60, + "statistic": "Average", + "threshold": 30, + "treat_missing_data": "notBreaching" + }, + "testPocketApp_autoscaling_scale_out_alarm_4313FBE9": { + "alarm_actions": [ + "\${aws_appautoscaling_policy.testPocketApp_autoscaling_scale_out_policy_075BFAC4.arn}" + ], + "alarm_description": "Alarm to add capacity if container CPU is high", + "alarm_name": "testapp Service High CPU", + "comparison_operator": "GreaterThanThreshold", + "dimensions": { + "ClusterName": "\${aws_ecs_cluster.testPocketApp_ecs_cluster_C3960066.name}", + "ServiceName": "\${aws_ecs_service.testPocketApp_ecs_service_ecs-service_182DEA4C.name}" + }, + "evaluation_periods": 2, + "metric_name": "CPUUtilization", + "namespace": "AWS/ECS", + "period": 60, + "statistic": "Average", + "threshold": 45, + "treat_missing_data": "notBreaching" + } + }, + "aws_ecr_lifecycle_policy": { + "testPocketApp_ecs_service_ecr-xray-daemon_ecr-repo-lifecyclepolicy_B1867100": { + "depends_on": [ + "aws_ecr_repository.testPocketApp_ecs_service_ecr-xray-daemon_ecr-repo_9028CD2C" + ], + "policy": "{\\"rules\\":[{\\"rulePriority\\":1,\\"description\\":\\"expire old images\\",\\"selection\\":{\\"tagStatus\\":\\"any\\",\\"countType\\":\\"imageCountMoreThan\\",\\"countNumber\\":800},\\"action\\":{\\"type\\":\\"expire\\"}}]}", + "repository": "\${aws_ecr_repository.testPocketApp_ecs_service_ecr-xray-daemon_ecr-repo_9028CD2C.name}" + } + }, + "aws_ecr_repository": { + "testPocketApp_ecs_service_ecr-xray-daemon_ecr-repo_9028CD2C": { + "image_scanning_configuration": { + "scan_on_push": true + }, + "name": "testapp-xray-daemon" + } + }, + "aws_ecs_cluster": { + "testPocketApp_ecs_cluster_C3960066": { + "name": "testapp", + "setting": [ + { + "name": "containerInsights", + "value": "enabled" + } + ] + } + }, + "aws_ecs_service": { + "testPocketApp_ecs_service_ecs-service_182DEA4C": { + "cluster": "\${aws_ecs_cluster.testPocketApp_ecs_cluster_C3960066.arn}", + "depends_on": [ + "aws_ecr_repository.testPocketApp_ecs_service_ecr-xray-daemon_ecr-repo_9028CD2C", + "aws_alb_target_group.testPocketApp_ecs_service_blue_target_group_ecs_target_group_A4B85885", + "aws_alb_listener_rule.testPocketApp_ecs_service_listener_rule_C03BB04D" + ], + "deployment_controller": { + "type": "ECS" + }, + "deployment_maximum_percent": 200, + "deployment_minimum_healthy_percent": 100, + "desired_count": 2, + "launch_type": "FARGATE", + "lifecycle": { + "ignore_changes": [ + "desired_count", + "load_balancer" + ] + }, + "load_balancer": [ + { + "container_name": "main_container", + "container_port": 8675309, + "target_group_arn": "\${aws_alb_target_group.testPocketApp_ecs_service_blue_target_group_ecs_target_group_A4B85885.arn}" + } + ], + "name": "testapp", + "network_configuration": { + "security_groups": [ + "\${aws_security_group.testPocketApp_ecs_service_ecs_security_group_9C016FA8.id}" + ], + "subnets": "\${data.aws_subnets.testPocketApp_pocket_vpc_private_subnet_ids_EB1E3A65.ids}" + }, + "propagate_tags": "SERVICE", + "task_definition": "\${aws_ecs_task_definition.testPocketApp_ecs_service_ecs-task_A7E74E45.arn}" + } + }, + "aws_ecs_task_definition": { + "testPocketApp_ecs_service_ecs-task_A7E74E45": { + "container_definitions": "[{\\"dnsSearchDomains\\":null,\\"environmentFiles\\":null,\\"logConfiguration\\":{\\"logDriver\\":\\"awslogs\\",\\"secretOptions\\":[],\\"options\\":{\\"awslogs-group\\":\\"\${aws_cloudwatch_log_group.testPocketApp_ecs_service_ecs-xray-daemon_55253A74.name}\\",\\"awslogs-region\\":\\"us-east-1\\",\\"awslogs-stream-prefix\\":\\"ecs\\"}},\\"entryPoint\\":null,\\"portMappings\\":[{\\"hostPort\\":0,\\"containerPort\\":2000,\\"protocol\\":\\"udp\\"}],\\"linuxParameters\\":null,\\"cpu\\":10,\\"environment\\":[],\\"resourceRequirements\\":null,\\"ulimits\\":null,\\"repositoryCredentials\\":null,\\"dnsServers\\":null,\\"mountPoints\\":[],\\"workingDirectory\\":null,\\"secrets\\":null,\\"dockerSecurityOptions\\":null,\\"memory\\":null,\\"memoryReservation\\":50,\\"volumesFrom\\":[],\\"stopTimeout\\":null,\\"image\\":\\"\${aws_ecr_repository.testPocketApp_ecs_service_ecr-xray-daemon_ecr-repo_9028CD2C.repository_url}:latest\\",\\"startTimeout\\":null,\\"firelensConfiguration\\":null,\\"dependsOn\\":null,\\"disableNetworking\\":null,\\"interactive\\":null,\\"healthCheck\\":null,\\"essential\\":true,\\"links\\":null,\\"hostname\\":null,\\"extraHosts\\":null,\\"pseudoTerminal\\":null,\\"user\\":null,\\"readonlyRootFilesystem\\":false,\\"dockerLabels\\":null,\\"systemControls\\":null,\\"privileged\\":null,\\"name\\":\\"xray-daemon\\"}]", + "cpu": "512", + "depends_on": [ + "aws_ecr_repository.testPocketApp_ecs_service_ecr-xray-daemon_ecr-repo_9028CD2C" + ], + "execution_role_arn": "\${aws_iam_role.testPocketApp_ecs_service_ecs-iam_ecs-execution-role_EB461A0C.arn}", + "family": "testapp", + "memory": "2048", + "network_mode": "awsvpc", + "requires_compatibilities": [ + "FARGATE" + ], + "task_role_arn": "\${aws_iam_role.testPocketApp_ecs_service_ecs-iam_ecs-task-role_7DB93963.arn}", + "volume": [ + ] + } + }, + "aws_iam_role": { + "testPocketApp_ecs_service_ecs-iam_ecs-execution-role_EB461A0C": { + "assume_role_policy": "\${data.aws_iam_policy_document.testPocketApp_ecs_service_ecs-iam_ecs-task-assume_ABB81680.json}", + "name": "testapp-TaskExecutionRole" + }, + "testPocketApp_ecs_service_ecs-iam_ecs-task-role_7DB93963": { + "assume_role_policy": "\${data.aws_iam_policy_document.testPocketApp_ecs_service_ecs-iam_ecs-task-assume_ABB81680.json}", + "name": "testapp-TaskRole" + } + }, + "aws_route53_record": { + "testPocketApp_alb_certificate_certificate_record_3C49201F": { + "depends_on": [ + "aws_acm_certificate.testPocketApp_alb_certificate_417C14FF" + ], + "name": "\${tolist(aws_acm_certificate.testPocketApp_alb_certificate_417C14FF.domain_validation_options).0.resource_record_name}", + "records": [ + "\${tolist(aws_acm_certificate.testPocketApp_alb_certificate_417C14FF.domain_validation_options).0.resource_record_value}" + ], + "ttl": 60, + "type": "\${tolist(aws_acm_certificate.testPocketApp_alb_certificate_417C14FF.domain_validation_options).0.resource_record_type}", + "zone_id": "\${aws_route53_zone.testPocketApp_base_dns_subhosted_zone_1E1E3243.zone_id}" + }, + "testPocketApp_alb_record_7B56ED33": { + "alias": { + "evaluate_target_health": true, + "name": "\${aws_alb.testPocketApp_application_load_balancer_alb_D538DEEE.dns_name}", + "zone_id": "\${aws_alb.testPocketApp_application_load_balancer_alb_D538DEEE.zone_id}" + }, + "lifecycle": { + "ignore_changes": [ + "weighted_routing_policy[0].weight" + ] + }, + "name": "testing.bowling.gov", + "set_identifier": "1", + "type": "A", + "weighted_routing_policy": { + "weight": 1 + }, + "zone_id": "\${aws_route53_zone.testPocketApp_base_dns_subhosted_zone_1E1E3243.zone_id}" + }, + "testPocketApp_base_dns_subhosted_zone_ns_497FFB83": { + "name": "testing.bowling.gov", + "records": "\${aws_route53_zone.testPocketApp_base_dns_subhosted_zone_1E1E3243.name_servers}", + "ttl": 86400, + "type": "NS", + "zone_id": "\${data.aws_route53_zone.testPocketApp_base_dns_main_hosted_zone_9442EEB0.zone_id}" + } + }, + "aws_route53_zone": { + "testPocketApp_base_dns_subhosted_zone_1E1E3243": { + "name": "testing.bowling.gov" + } + }, + "aws_security_group": { + "testPocketApp_application_load_balancer_alb_security_group_91A27046": { + "description": "External security group (Managed by Terraform)", + "egress": [ + { + "cidr_blocks": [ + "0.0.0.0/0" + ], + "description": "required", + "from_port": 0, + "ipv6_cidr_blocks": [ + ], + "prefix_list_ids": [ + ], + "protocol": "-1", + "security_groups": [ + ], + "self": null, + "to_port": 0 + } + ], + "ingress": [ + { + "cidr_blocks": [ + "0.0.0.0/0" + ], + "description": null, + "from_port": 443, + "ipv6_cidr_blocks": null, + "prefix_list_ids": null, + "protocol": "TCP", + "security_groups": null, + "self": null, + "to_port": 443 + }, + { + "cidr_blocks": [ + "0.0.0.0/0" + ], + "description": null, + "from_port": 80, + "ipv6_cidr_blocks": null, + "prefix_list_ids": null, + "protocol": "TCP", + "security_groups": null, + "self": null, + "to_port": 80 + } + ], + "lifecycle": { + "create_before_destroy": true + }, + "name_prefix": "testapp-HTTP/S Security Group", + "tags": { + "Name": "testapp-HTTP/S Security Group" + }, + "vpc_id": "\${data.aws_vpc.testPocketApp_pocket_vpc_C4E157E3.id}" + }, + "testPocketApp_ecs_service_ecs_security_group_9C016FA8": { + "description": "Internal ECS Security Group (Managed by Terraform)", + "egress": [ + { + "cidr_blocks": [ + "0.0.0.0/0" + ], + "description": "required", + "from_port": 0, + "ipv6_cidr_blocks": [ + ], + "prefix_list_ids": [ + ], + "protocol": "-1", + "security_groups": [ + ], + "self": null, + "to_port": 0 + } + ], + "ingress": [ + { + "cidr_blocks": [ + ], + "description": "required", + "from_port": 80, + "ipv6_cidr_blocks": [ + ], + "prefix_list_ids": [ + ], + "protocol": "TCP", + "security_groups": [ + "\${aws_security_group.testPocketApp_application_load_balancer_alb_security_group_91A27046.id}" + ], + "self": null, + "to_port": 8675309 + } + ], + "lifecycle": { + "create_before_destroy": true + }, + "name_prefix": "testapp-ECSSecurityGroup", + "vpc_id": "\${data.aws_vpc.testPocketApp_pocket_vpc_C4E157E3.id}" + } + } + } +}" +`; + +exports[`PocketALBApplication renders an external application 1`] = ` +"{ + "data": { + "aws_caller_identity": { + "testPocketApp_pocket_vpc_current_identity_06D87057": { + } + }, + "aws_iam_policy_document": { + "testPocketApp_ecs_service_ecs-iam_ecs-task-assume_ABB81680": { + "statement": [ + { + "actions": [ + "sts:AssumeRole" + ], + "effect": "Allow", + "principals": [ + { + "identifiers": [ + "ecs-tasks.amazonaws.com" + ], + "type": "Service" + } + ] + } + ], + "version": "2012-10-17" + } + }, + "aws_kms_alias": { + "testPocketApp_pocket_vpc_secrets_manager_key_9FD2BC93": { + "name": "alias/aws/secretsmanager" + } + }, + "aws_region": { + "testPocketApp_pocket_vpc_current_region_8ED435E7": { + } + }, + "aws_route53_zone": { + "testPocketApp_base_dns_main_hosted_zone_9442EEB0": { + "name": "bowling.gov" + } + }, + "aws_security_groups": { + "testPocketApp_pocket_vpc_default_security_groups_40B4FC48": { + "filter": [ + { + "name": "group-name", + "values": [ + "default" + ] + }, + { + "name": "vpc-id", + "values": [ + "\${data.aws_vpc.testPocketApp_pocket_vpc_C4E157E3.id}" + ] + } + ] + }, + "testPocketApp_pocket_vpc_internal_security_groups_12F7F666": { + "filter": [ + { + "name": "group-name", + "values": [ + "pocket-vpc-internal" + ] + }, + { + "name": "vpc-id", + "values": [ + "\${data.aws_vpc.testPocketApp_pocket_vpc_C4E157E3.id}" + ] + } + ] + } + }, + "aws_ssm_parameter": { + "testPocketApp_pocket_vpc_private_subnets_9D449563": { + "name": "/Shared/PrivateSubnets" + }, + "testPocketApp_pocket_vpc_public_subnets_B0B3A6AB": { + "name": "/Shared/PublicSubnets" + }, + "testPocketApp_pocket_vpc_vpc_ssm_param_235525D4": { + "name": "/Shared/Vpc" + } + }, + "aws_subnets": { + "testPocketApp_pocket_vpc_private_subnet_ids_EB1E3A65": { + "filter": [ + { + "name": "subnet-id", + "values": "\${split(\\",\\", data.aws_ssm_parameter.testPocketApp_pocket_vpc_private_subnets_9D449563.value)}" + }, + { + "name": "vpc-id", + "values": [ + "\${data.aws_vpc.testPocketApp_pocket_vpc_C4E157E3.id}" + ] + } + ] + }, + "testPocketApp_pocket_vpc_public_subnet_ids_01F0B902": { + "filter": [ + { + "name": "subnet-id", + "values": "\${split(\\",\\", data.aws_ssm_parameter.testPocketApp_pocket_vpc_public_subnets_B0B3A6AB.value)}" + }, + { + "name": "vpc-id", + "values": [ + "\${data.aws_vpc.testPocketApp_pocket_vpc_C4E157E3.id}" + ] + } + ] + } + }, + "aws_vpc": { + "testPocketApp_pocket_vpc_C4E157E3": { + "filter": [ + { + "name": "vpc-id", + "values": [ + "\${data.aws_ssm_parameter.testPocketApp_pocket_vpc_vpc_ssm_param_235525D4.value}" + ] + } + ] + } + } + }, + "resource": { + "aws_acm_certificate": { + "testPocketApp_alb_certificate_417C14FF": { + "domain_name": "direct.testing.bowling.gov", + "lifecycle": { + "create_before_destroy": true + }, + "validation_method": "DNS" + }, + "testPocketApp_cdn_certificate_F1CBB9BB": { + "domain_name": "testing.bowling.gov", + "lifecycle": { + "create_before_destroy": true + }, + "validation_method": "DNS" + } + }, + "aws_acm_certificate_validation": { + "testPocketApp_alb_certificate_certificate_validation_7D899C6A": { + "certificate_arn": "\${aws_acm_certificate.testPocketApp_alb_certificate_417C14FF.arn}", + "depends_on": [ + "aws_route53_record.testPocketApp_alb_certificate_certificate_record_3C49201F", + "aws_acm_certificate.testPocketApp_alb_certificate_417C14FF" + ], + "validation_record_fqdns": [ + "\${aws_route53_record.testPocketApp_alb_certificate_certificate_record_3C49201F.fqdn}" + ] + }, + "testPocketApp_cdn_certificate_certificate_validation_BA2932DC": { + "certificate_arn": "\${aws_acm_certificate.testPocketApp_cdn_certificate_F1CBB9BB.arn}", + "depends_on": [ + "aws_route53_record.testPocketApp_cdn_certificate_certificate_record_63BAB662", + "aws_acm_certificate.testPocketApp_cdn_certificate_F1CBB9BB" + ], + "validation_record_fqdns": [ + "\${aws_route53_record.testPocketApp_cdn_certificate_certificate_record_63BAB662.fqdn}" + ] + } + }, + "aws_alb": { + "testPocketApp_application_load_balancer_alb_D538DEEE": { + "internal": false, + "name_prefix": "TSTAPP", + "security_groups": [ + "\${aws_security_group.testPocketApp_application_load_balancer_alb_security_group_91A27046.id}" + ], + "subnets": "\${data.aws_subnets.testPocketApp_pocket_vpc_public_subnet_ids_01F0B902.ids}" + } + }, + "aws_alb_listener": { + "testPocketApp_listener_http_A9E227C2": { + "default_action": [ + { + "redirect": { + "port": "443", + "protocol": "HTTPS", + "status_code": "HTTP_301" + }, + "type": "redirect" + } + ], + "load_balancer_arn": "\${aws_alb.testPocketApp_application_load_balancer_alb_D538DEEE.arn}", + "port": 80, + "protocol": "HTTP" + }, + "testPocketApp_listener_https_7F49DE83": { + "certificate_arn": "\${aws_acm_certificate.testPocketApp_alb_certificate_417C14FF.arn}", + "default_action": [ + { + "fixed_response": { + "content_type": "text/plain", + "message_body": "", + "status_code": "503" + }, + "type": "fixed-response" + } + ], + "load_balancer_arn": "\${aws_alb.testPocketApp_application_load_balancer_alb_D538DEEE.arn}", + "port": 443, + "protocol": "HTTPS", + "ssl_policy": "ELBSecurityPolicy-TLS-1-1-2017-01" + } + }, + "aws_alb_listener_rule": { + "testPocketApp_ecs_service_listener_rule_C03BB04D": { + "action": [ + { + "target_group_arn": "\${aws_alb_target_group.testPocketApp_ecs_service_blue_target_group_ecs_target_group_A4B85885.arn}", + "type": "forward" + } + ], + "condition": [ + { + "path_pattern": { + "values": [ + "*" + ] + } + } + ], + "lifecycle": { + "ignore_changes": [ + "action" + ] + }, + "listener_arn": "\${aws_alb_listener.testPocketApp_listener_https_7F49DE83.arn}", + "priority": 1 + } + }, + "aws_alb_target_group": { + "testPocketApp_ecs_service_blue_target_group_ecs_target_group_A4B85885": { + "deregistration_delay": "120", + "health_check": { + "healthy_threshold": 5, + "interval": 15, + "path": "/test", + "protocol": "HTTP", + "unhealthy_threshold": 3 + }, + "name_prefix": "TSTAPP", + "port": 80, + "protocol": "HTTP", + "tags": { + "type": "blue" + }, + "target_type": "ip", + "vpc_id": "\${data.aws_vpc.testPocketApp_pocket_vpc_C4E157E3.id}" + } + }, + "aws_appautoscaling_policy": { + "testPocketApp_autoscaling_scale_in_policy_A163A235": { + "depends_on": [ + "aws_appautoscaling_target.testPocketApp_autoscaling_autoscaling_target_505E4EA1" + ], + "name": "testapp-ScaleInPolicy", + "policy_type": "StepScaling", + "resource_id": "\${aws_appautoscaling_target.testPocketApp_autoscaling_autoscaling_target_505E4EA1.resource_id}", + "scalable_dimension": "\${aws_appautoscaling_target.testPocketApp_autoscaling_autoscaling_target_505E4EA1.scalable_dimension}", + "service_namespace": "\${aws_appautoscaling_target.testPocketApp_autoscaling_autoscaling_target_505E4EA1.service_namespace}", + "step_scaling_policy_configuration": { + "adjustment_type": "ChangeInCapacity", + "cooldown": 60, + "metric_aggregation_type": "Average", + "step_adjustment": [ + { + "metric_interval_upper_bound": "0", + "scaling_adjustment": -1 + } + ] + }, + "target_tracking_scaling_policy_configuration": null + }, + "testPocketApp_autoscaling_scale_out_policy_075BFAC4": { + "depends_on": [ + "aws_appautoscaling_target.testPocketApp_autoscaling_autoscaling_target_505E4EA1" + ], + "name": "testapp-ScaleOutPolicy", + "policy_type": "StepScaling", + "resource_id": "\${aws_appautoscaling_target.testPocketApp_autoscaling_autoscaling_target_505E4EA1.resource_id}", + "scalable_dimension": "\${aws_appautoscaling_target.testPocketApp_autoscaling_autoscaling_target_505E4EA1.scalable_dimension}", + "service_namespace": "\${aws_appautoscaling_target.testPocketApp_autoscaling_autoscaling_target_505E4EA1.service_namespace}", + "step_scaling_policy_configuration": { + "adjustment_type": "ChangeInCapacity", + "cooldown": 60, + "metric_aggregation_type": "Average", + "step_adjustment": [ + { + "metric_interval_lower_bound": "0", + "scaling_adjustment": 2 + } + ] + }, + "target_tracking_scaling_policy_configuration": null + } + }, + "aws_appautoscaling_target": { + "testPocketApp_autoscaling_autoscaling_target_505E4EA1": { + "max_capacity": 2, + "min_capacity": 1, + "resource_id": "service/\${aws_ecs_cluster.testPocketApp_ecs_cluster_C3960066.name}/\${aws_ecs_service.testPocketApp_ecs_service_ecs-service_182DEA4C.name}", + "scalable_dimension": "ecs:service:DesiredCount", + "service_namespace": "ecs" + } + }, + "aws_cloudfront_distribution": { + "testPocketApp_cloudfront_distribution_FD7F01BF": { + "aliases": [ + "testing.bowling.gov" + ], + "comment": "CDN for direct.testing.bowling.gov", + "default_cache_behavior": { + "allowed_methods": [ + "GET", + "HEAD", + "OPTIONS", + "PUT", + "POST", + "PATCH", + "DELETE" + ], + "cached_methods": [ + "GET", + "HEAD", + "OPTIONS" + ], + "compress": true, + "default_ttl": 0, + "forwarded_values": { + "cookies": { + "forward": "none" + }, + "headers": [ + "Accept", + "Origin", + "Authorization" + ], + "query_string": true + }, + "max_ttl": 31536000, + "min_ttl": 0, + "target_origin_id": "Alb", + "viewer_protocol_policy": "redirect-to-https" + }, + "enabled": true, + "origin": [ + { + "custom_origin_config": { + "http_port": 80, + "https_port": 443, + "origin_protocol_policy": "https-only", + "origin_ssl_protocols": [ + "TLSv1.1", + "TLSv1.2" + ] + }, + "domain_name": "\${aws_route53_record.testPocketApp_alb_record_7B56ED33.fqdn}", + "origin_id": "Alb" + } + ], + "price_class": "PriceClass_200", + "restrictions": { + "geo_restriction": { + "restriction_type": "none" + } + }, + "viewer_certificate": { + "acm_certificate_arn": "\${aws_acm_certificate.testPocketApp_cdn_certificate_F1CBB9BB.arn}", + "minimum_protocol_version": "TLSv1.1_2016", + "ssl_support_method": "sni-only" + } + } + }, + "aws_cloudwatch_dashboard": { + "testPocketApp_cloudwatch-dashboard_26E9F70C": { + "dashboard_body": "{\\"widgets\\":[{\\"type\\":\\"metric\\",\\"x\\":0,\\"y\\":0,\\"width\\":12,\\"height\\":6,\\"properties\\":{\\"metrics\\":[[\\"AWS/ApplicationELB\\",\\"HTTPCode_Target_4XX_Count\\",\\"LoadBalancer\\",\\"\${aws_alb.testPocketApp_application_load_balancer_alb_D538DEEE.arn_suffix}\\",{\\"yAxis\\":\\"left\\",\\"color\\":\\"#ff7f0e\\"}],[\\".\\",\\"RequestCount\\",\\".\\",\\".\\",{\\"yAxis\\":\\"right\\",\\"color\\":\\"#1f77b4\\"}],[\\".\\",\\"HTTPCode_Target_5XX_Count\\",\\".\\",\\".\\",{\\"color\\":\\"#d62728\\"}],[\\".\\",\\"HTTPCode_Target_2XX_Count\\",\\".\\",\\".\\",{\\"yAxis\\":\\"right\\",\\"color\\":\\"#2ca02c\\"}]],\\"view\\":\\"timeSeries\\",\\"stacked\\":false,\\"region\\":\\"us-east-1\\",\\"period\\":60,\\"stat\\":\\"Sum\\",\\"title\\":\\"Target Requests\\"}},{\\"type\\":\\"metric\\",\\"x\\":12,\\"y\\":0,\\"width\\":12,\\"height\\":6,\\"properties\\":{\\"metrics\\":[[\\"AWS/ApplicationELB\\",\\"HTTPCode_ELB_4XX_Count\\",\\"LoadBalancer\\",\\"\${aws_alb.testPocketApp_application_load_balancer_alb_D538DEEE.arn_suffix}\\",{\\"yAxis\\":\\"left\\",\\"color\\":\\"#ff7f0e\\"}],[\\".\\",\\"RequestCount\\",\\".\\",\\".\\",{\\"yAxis\\":\\"right\\",\\"color\\":\\"#1f77b4\\"}],[\\".\\",\\"HTTPCode_ELB_5XX_Count\\",\\".\\",\\".\\",{\\"color\\":\\"#d62728\\"}]],\\"view\\":\\"timeSeries\\",\\"stacked\\":false,\\"region\\":\\"us-east-1\\",\\"period\\":60,\\"stat\\":\\"Sum\\",\\"title\\":\\"ALB Requests\\"}},{\\"type\\":\\"metric\\",\\"x\\":12,\\"y\\":6,\\"width\\":12,\\"height\\":6,\\"properties\\":{\\"metrics\\":[[\\"AWS/ApplicationELB\\",\\"TargetResponseTime\\",\\"LoadBalancer\\",\\"\${aws_alb.testPocketApp_application_load_balancer_alb_D538DEEE.arn_suffix}\\",{\\"label\\":\\"Average\\",\\"color\\":\\"#aec7e8\\"}],[\\"...\\",{\\"stat\\":\\"p95\\",\\"label\\":\\"p95\\",\\"color\\":\\"#ffbb78\\"}],[\\"...\\",{\\"stat\\":\\"p99\\",\\"label\\":\\"p99\\",\\"color\\":\\"#98df8a\\"}]],\\"view\\":\\"timeSeries\\",\\"stacked\\":false,\\"region\\":\\"us-east-1\\",\\"stat\\":\\"Average\\",\\"period\\":60}},{\\"type\\":\\"metric\\",\\"x\\":0,\\"y\\":6,\\"width\\":12,\\"height\\":6,\\"properties\\":{\\"metrics\\":[[\\"ECS/ContainerInsights\\",\\"RunningTaskCount\\",\\"ServiceName\\",\\"\${aws_ecs_service.testPocketApp_ecs_service_ecs-service_182DEA4C.name}\\",\\"ClusterName\\",\\"\${aws_ecs_cluster.testPocketApp_ecs_cluster_C3960066.name}\\",{\\"yAxis\\":\\"right\\",\\"color\\":\\"#c49c94\\"}],[\\"AWS/ECS\\",\\"CPUUtilization\\",\\".\\",\\".\\",\\".\\",\\".\\",{\\"color\\":\\"#f7b6d2\\"}],[\\".\\",\\"MemoryUtilization\\",\\".\\",\\".\\",\\".\\",\\".\\",{\\"color\\":\\"#c7c7c7\\"}]],\\"view\\":\\"timeSeries\\",\\"stacked\\":false,\\"region\\":\\"us-east-1\\",\\"stat\\":\\"Average\\",\\"period\\":60,\\"annotations\\":{\\"horizontal\\":[{\\"color\\":\\"#e377c2\\",\\"label\\":\\"CPU scale out\\",\\"value\\":45},{\\"color\\":\\"#c5b0d5\\",\\"label\\":\\"CPU scale in\\",\\"value\\":30}]},\\"title\\":\\"Service Load\\"}}]}", + "dashboard_name": "testapp", + "lifecycle": { + "ignore_changes": [ + "dashboard_body" + ] + } + } + }, + "aws_cloudwatch_metric_alarm": { + "testPocketApp_autoscaling_scale_in_alarm_69B5F00D": { + "alarm_actions": [ + "\${aws_appautoscaling_policy.testPocketApp_autoscaling_scale_in_policy_A163A235.arn}" + ], + "alarm_description": "Alarm to reduce capacity if container CPU is low", + "alarm_name": "testapp Service Low CPU", + "comparison_operator": "LessThanThreshold", + "dimensions": { + "ClusterName": "\${aws_ecs_cluster.testPocketApp_ecs_cluster_C3960066.name}", + "ServiceName": "\${aws_ecs_service.testPocketApp_ecs_service_ecs-service_182DEA4C.name}" + }, + "evaluation_periods": 2, + "metric_name": "CPUUtilization", + "namespace": "AWS/ECS", + "period": 60, + "statistic": "Average", + "threshold": 30, + "treat_missing_data": "notBreaching" + }, + "testPocketApp_autoscaling_scale_out_alarm_4313FBE9": { + "alarm_actions": [ + "\${aws_appautoscaling_policy.testPocketApp_autoscaling_scale_out_policy_075BFAC4.arn}" + ], + "alarm_description": "Alarm to add capacity if container CPU is high", + "alarm_name": "testapp Service High CPU", + "comparison_operator": "GreaterThanThreshold", + "dimensions": { + "ClusterName": "\${aws_ecs_cluster.testPocketApp_ecs_cluster_C3960066.name}", + "ServiceName": "\${aws_ecs_service.testPocketApp_ecs_service_ecs-service_182DEA4C.name}" + }, + "evaluation_periods": 2, + "metric_name": "CPUUtilization", + "namespace": "AWS/ECS", + "period": 60, + "statistic": "Average", + "threshold": 45, + "treat_missing_data": "notBreaching" + } + }, + "aws_ecs_cluster": { + "testPocketApp_ecs_cluster_C3960066": { + "name": "testapp", + "setting": [ + { + "name": "containerInsights", + "value": "enabled" + } + ] + } + }, + "aws_ecs_service": { + "testPocketApp_ecs_service_ecs-service_182DEA4C": { + "cluster": "\${aws_ecs_cluster.testPocketApp_ecs_cluster_C3960066.arn}", + "depends_on": [ + "aws_alb_target_group.testPocketApp_ecs_service_blue_target_group_ecs_target_group_A4B85885", + "aws_alb_listener_rule.testPocketApp_ecs_service_listener_rule_C03BB04D" + ], + "deployment_controller": { + "type": "ECS" + }, + "deployment_maximum_percent": 200, + "deployment_minimum_healthy_percent": 100, + "desired_count": 2, + "launch_type": "FARGATE", + "lifecycle": { + "ignore_changes": [ + "desired_count", + "load_balancer" + ] + }, + "load_balancer": [ + { + "container_name": "main_container", + "container_port": 8675309, + "target_group_arn": "\${aws_alb_target_group.testPocketApp_ecs_service_blue_target_group_ecs_target_group_A4B85885.arn}" + } + ], + "name": "testapp", + "network_configuration": { + "security_groups": [ + "\${aws_security_group.testPocketApp_ecs_service_ecs_security_group_9C016FA8.id}" + ], + "subnets": "\${data.aws_subnets.testPocketApp_pocket_vpc_private_subnet_ids_EB1E3A65.ids}" + }, + "propagate_tags": "SERVICE", + "task_definition": "\${aws_ecs_task_definition.testPocketApp_ecs_service_ecs-task_A7E74E45.arn}" + } + }, + "aws_ecs_task_definition": { + "testPocketApp_ecs_service_ecs-task_A7E74E45": { + "container_definitions": "[]", + "cpu": "512", + "depends_on": [ + ], + "execution_role_arn": "\${aws_iam_role.testPocketApp_ecs_service_ecs-iam_ecs-execution-role_EB461A0C.arn}", + "family": "testapp", + "memory": "2048", + "network_mode": "awsvpc", + "requires_compatibilities": [ + "FARGATE" + ], + "task_role_arn": "\${aws_iam_role.testPocketApp_ecs_service_ecs-iam_ecs-task-role_7DB93963.arn}", + "volume": [ + ] + } + }, + "aws_iam_role": { + "testPocketApp_ecs_service_ecs-iam_ecs-execution-role_EB461A0C": { + "assume_role_policy": "\${data.aws_iam_policy_document.testPocketApp_ecs_service_ecs-iam_ecs-task-assume_ABB81680.json}", + "name": "testapp-TaskExecutionRole" + }, + "testPocketApp_ecs_service_ecs-iam_ecs-task-role_7DB93963": { + "assume_role_policy": "\${data.aws_iam_policy_document.testPocketApp_ecs_service_ecs-iam_ecs-task-assume_ABB81680.json}", + "name": "testapp-TaskRole" + } + }, + "aws_route53_record": { + "testPocketApp_alb_certificate_certificate_record_3C49201F": { + "depends_on": [ + "aws_acm_certificate.testPocketApp_alb_certificate_417C14FF" + ], + "name": "\${tolist(aws_acm_certificate.testPocketApp_alb_certificate_417C14FF.domain_validation_options).0.resource_record_name}", + "records": [ + "\${tolist(aws_acm_certificate.testPocketApp_alb_certificate_417C14FF.domain_validation_options).0.resource_record_value}" + ], + "ttl": 60, + "type": "\${tolist(aws_acm_certificate.testPocketApp_alb_certificate_417C14FF.domain_validation_options).0.resource_record_type}", + "zone_id": "\${aws_route53_zone.testPocketApp_base_dns_subhosted_zone_1E1E3243.zone_id}" + }, + "testPocketApp_alb_record_7B56ED33": { + "alias": { + "evaluate_target_health": true, + "name": "\${aws_alb.testPocketApp_application_load_balancer_alb_D538DEEE.dns_name}", + "zone_id": "\${aws_alb.testPocketApp_application_load_balancer_alb_D538DEEE.zone_id}" + }, + "lifecycle": { + "ignore_changes": [ + "weighted_routing_policy[0].weight" + ] + }, + "name": "direct.testing.bowling.gov", + "set_identifier": "1", + "type": "A", + "weighted_routing_policy": { + "weight": 1 + }, + "zone_id": "\${aws_route53_zone.testPocketApp_base_dns_subhosted_zone_1E1E3243.zone_id}" + }, + "testPocketApp_base_dns_subhosted_zone_ns_497FFB83": { + "name": "testing.bowling.gov", + "records": "\${aws_route53_zone.testPocketApp_base_dns_subhosted_zone_1E1E3243.name_servers}", + "ttl": 86400, + "type": "NS", + "zone_id": "\${data.aws_route53_zone.testPocketApp_base_dns_main_hosted_zone_9442EEB0.zone_id}" + }, + "testPocketApp_cdn_certificate_certificate_record_63BAB662": { + "depends_on": [ + "aws_acm_certificate.testPocketApp_cdn_certificate_F1CBB9BB" + ], + "name": "\${tolist(aws_acm_certificate.testPocketApp_cdn_certificate_F1CBB9BB.domain_validation_options).0.resource_record_name}", + "records": [ + "\${tolist(aws_acm_certificate.testPocketApp_cdn_certificate_F1CBB9BB.domain_validation_options).0.resource_record_value}" + ], + "ttl": 60, + "type": "\${tolist(aws_acm_certificate.testPocketApp_cdn_certificate_F1CBB9BB.domain_validation_options).0.resource_record_type}", + "zone_id": "\${aws_route53_zone.testPocketApp_base_dns_subhosted_zone_1E1E3243.zone_id}" + }, + "testPocketApp_cdn_record_DD10AA15": { + "alias": { + "evaluate_target_health": true, + "name": "\${aws_cloudfront_distribution.testPocketApp_cloudfront_distribution_FD7F01BF.domain_name}", + "zone_id": "\${aws_cloudfront_distribution.testPocketApp_cloudfront_distribution_FD7F01BF.hosted_zone_id}" + }, + "lifecycle": { + "ignore_changes": [ + "weighted_routing_policy[0].weight" + ] + }, + "name": "testing.bowling.gov", + "set_identifier": "2", + "type": "A", + "weighted_routing_policy": { + "weight": 1 + }, + "zone_id": "\${aws_route53_zone.testPocketApp_base_dns_subhosted_zone_1E1E3243.zone_id}" + } + }, + "aws_route53_zone": { + "testPocketApp_base_dns_subhosted_zone_1E1E3243": { + "name": "testing.bowling.gov" + } + }, + "aws_security_group": { + "testPocketApp_application_load_balancer_alb_security_group_91A27046": { + "description": "External security group (Managed by Terraform)", + "egress": [ + { + "cidr_blocks": [ + "0.0.0.0/0" + ], + "description": "required", + "from_port": 0, + "ipv6_cidr_blocks": [ + ], + "prefix_list_ids": [ + ], + "protocol": "-1", + "security_groups": [ + ], + "self": null, + "to_port": 0 + } + ], + "ingress": [ + { + "cidr_blocks": [ + "0.0.0.0/0" + ], + "description": null, + "from_port": 443, + "ipv6_cidr_blocks": null, + "prefix_list_ids": null, + "protocol": "TCP", + "security_groups": null, + "self": null, + "to_port": 443 + }, + { + "cidr_blocks": [ + "0.0.0.0/0" + ], + "description": null, + "from_port": 80, + "ipv6_cidr_blocks": null, + "prefix_list_ids": null, + "protocol": "TCP", + "security_groups": null, + "self": null, + "to_port": 80 + } + ], + "lifecycle": { + "create_before_destroy": true + }, + "name_prefix": "testapp-HTTP/S Security Group", + "tags": { + "Name": "testapp-HTTP/S Security Group" + }, + "vpc_id": "\${data.aws_vpc.testPocketApp_pocket_vpc_C4E157E3.id}" + }, + "testPocketApp_ecs_service_ecs_security_group_9C016FA8": { + "description": "Internal ECS Security Group (Managed by Terraform)", + "egress": [ + { + "cidr_blocks": [ + "0.0.0.0/0" + ], + "description": "required", + "from_port": 0, + "ipv6_cidr_blocks": [ + ], + "prefix_list_ids": [ + ], + "protocol": "-1", + "security_groups": [ + ], + "self": null, + "to_port": 0 + } + ], + "ingress": [ + { + "cidr_blocks": [ + ], + "description": "required", + "from_port": 80, + "ipv6_cidr_blocks": [ + ], + "prefix_list_ids": [ + ], + "protocol": "TCP", + "security_groups": [ + "\${aws_security_group.testPocketApp_application_load_balancer_alb_security_group_91A27046.id}" + ], + "self": null, + "to_port": 8675309 + } + ], + "lifecycle": { + "create_before_destroy": true + }, + "name_prefix": "testapp-ECSSecurityGroup", + "vpc_id": "\${data.aws_vpc.testPocketApp_pocket_vpc_C4E157E3.id}" + } + } + } +}" +`; + +exports[`PocketALBApplication renders an internal application 1`] = ` +"{ + "data": { + "aws_caller_identity": { + "testPocketApp_pocket_vpc_current_identity_06D87057": { + } + }, + "aws_iam_policy_document": { + "testPocketApp_ecs_service_ecs-iam_ecs-task-assume_ABB81680": { + "statement": [ + { + "actions": [ + "sts:AssumeRole" + ], + "effect": "Allow", + "principals": [ + { + "identifiers": [ + "ecs-tasks.amazonaws.com" + ], + "type": "Service" + } + ] + } + ], + "version": "2012-10-17" + } + }, + "aws_kms_alias": { + "testPocketApp_pocket_vpc_secrets_manager_key_9FD2BC93": { + "name": "alias/aws/secretsmanager" + } + }, + "aws_region": { + "testPocketApp_pocket_vpc_current_region_8ED435E7": { + } + }, + "aws_route53_zone": { + "testPocketApp_base_dns_main_hosted_zone_9442EEB0": { + "name": "bowling.gov" + } + }, + "aws_security_groups": { + "testPocketApp_pocket_vpc_default_security_groups_40B4FC48": { + "filter": [ + { + "name": "group-name", + "values": [ + "default" + ] + }, + { + "name": "vpc-id", + "values": [ + "\${data.aws_vpc.testPocketApp_pocket_vpc_C4E157E3.id}" + ] + } + ] + }, + "testPocketApp_pocket_vpc_internal_security_groups_12F7F666": { + "filter": [ + { + "name": "group-name", + "values": [ + "pocket-vpc-internal" + ] + }, + { + "name": "vpc-id", + "values": [ + "\${data.aws_vpc.testPocketApp_pocket_vpc_C4E157E3.id}" + ] + } + ] + } + }, + "aws_ssm_parameter": { + "testPocketApp_pocket_vpc_private_subnets_9D449563": { + "name": "/Shared/PrivateSubnets" + }, + "testPocketApp_pocket_vpc_public_subnets_B0B3A6AB": { + "name": "/Shared/PublicSubnets" + }, + "testPocketApp_pocket_vpc_vpc_ssm_param_235525D4": { + "name": "/Shared/Vpc" + } + }, + "aws_subnets": { + "testPocketApp_pocket_vpc_private_subnet_ids_EB1E3A65": { + "filter": [ + { + "name": "subnet-id", + "values": "\${split(\\",\\", data.aws_ssm_parameter.testPocketApp_pocket_vpc_private_subnets_9D449563.value)}" + }, + { + "name": "vpc-id", + "values": [ + "\${data.aws_vpc.testPocketApp_pocket_vpc_C4E157E3.id}" + ] + } + ] + }, + "testPocketApp_pocket_vpc_public_subnet_ids_01F0B902": { + "filter": [ + { + "name": "subnet-id", + "values": "\${split(\\",\\", data.aws_ssm_parameter.testPocketApp_pocket_vpc_public_subnets_B0B3A6AB.value)}" + }, + { + "name": "vpc-id", + "values": [ + "\${data.aws_vpc.testPocketApp_pocket_vpc_C4E157E3.id}" + ] + } + ] + } + }, + "aws_vpc": { + "testPocketApp_pocket_vpc_C4E157E3": { + "filter": [ + { + "name": "vpc-id", + "values": [ + "\${data.aws_ssm_parameter.testPocketApp_pocket_vpc_vpc_ssm_param_235525D4.value}" + ] + } + ] + } + } + }, + "resource": { + "aws_acm_certificate": { + "testPocketApp_alb_certificate_417C14FF": { + "domain_name": "testing.bowling.gov", + "lifecycle": { + "create_before_destroy": true + }, + "validation_method": "DNS" + } + }, + "aws_acm_certificate_validation": { + "testPocketApp_alb_certificate_certificate_validation_7D899C6A": { + "certificate_arn": "\${aws_acm_certificate.testPocketApp_alb_certificate_417C14FF.arn}", + "depends_on": [ + "aws_route53_record.testPocketApp_alb_certificate_certificate_record_3C49201F", + "aws_acm_certificate.testPocketApp_alb_certificate_417C14FF" + ], + "validation_record_fqdns": [ + "\${aws_route53_record.testPocketApp_alb_certificate_certificate_record_3C49201F.fqdn}" + ] + } + }, + "aws_alb": { + "testPocketApp_application_load_balancer_alb_D538DEEE": { + "internal": true, + "name_prefix": "TSTAPP", + "security_groups": [ + "\${aws_security_group.testPocketApp_application_load_balancer_alb_security_group_91A27046.id}" + ], + "subnets": "\${data.aws_subnets.testPocketApp_pocket_vpc_private_subnet_ids_EB1E3A65.ids}" + } + }, + "aws_alb_listener": { + "testPocketApp_listener_http_A9E227C2": { + "default_action": [ + { + "redirect": { + "port": "443", + "protocol": "HTTPS", + "status_code": "HTTP_301" + }, + "type": "redirect" + } + ], + "load_balancer_arn": "\${aws_alb.testPocketApp_application_load_balancer_alb_D538DEEE.arn}", + "port": 80, + "protocol": "HTTP" + }, + "testPocketApp_listener_https_7F49DE83": { + "certificate_arn": "\${aws_acm_certificate.testPocketApp_alb_certificate_417C14FF.arn}", + "default_action": [ + { + "fixed_response": { + "content_type": "text/plain", + "message_body": "", + "status_code": "503" + }, + "type": "fixed-response" + } + ], + "load_balancer_arn": "\${aws_alb.testPocketApp_application_load_balancer_alb_D538DEEE.arn}", + "port": 443, + "protocol": "HTTPS", + "ssl_policy": "ELBSecurityPolicy-TLS-1-1-2017-01" + } + }, + "aws_alb_listener_rule": { + "testPocketApp_ecs_service_listener_rule_C03BB04D": { + "action": [ + { + "target_group_arn": "\${aws_alb_target_group.testPocketApp_ecs_service_blue_target_group_ecs_target_group_A4B85885.arn}", + "type": "forward" + } + ], + "condition": [ + { + "path_pattern": { + "values": [ + "*" + ] + } + } + ], + "lifecycle": { + "ignore_changes": [ + "action" + ] + }, + "listener_arn": "\${aws_alb_listener.testPocketApp_listener_https_7F49DE83.arn}", + "priority": 1 + } + }, + "aws_alb_target_group": { + "testPocketApp_ecs_service_blue_target_group_ecs_target_group_A4B85885": { + "deregistration_delay": "120", + "health_check": { + "healthy_threshold": 5, + "interval": 15, + "path": "/test", + "protocol": "HTTP", + "unhealthy_threshold": 3 + }, + "name_prefix": "TSTAPP", + "port": 80, + "protocol": "HTTP", + "tags": { + "type": "blue" + }, + "target_type": "ip", + "vpc_id": "\${data.aws_vpc.testPocketApp_pocket_vpc_C4E157E3.id}" + } + }, + "aws_appautoscaling_policy": { + "testPocketApp_autoscaling_scale_in_policy_A163A235": { + "depends_on": [ + "aws_appautoscaling_target.testPocketApp_autoscaling_autoscaling_target_505E4EA1" + ], + "name": "testapp-ScaleInPolicy", + "policy_type": "StepScaling", + "resource_id": "\${aws_appautoscaling_target.testPocketApp_autoscaling_autoscaling_target_505E4EA1.resource_id}", + "scalable_dimension": "\${aws_appautoscaling_target.testPocketApp_autoscaling_autoscaling_target_505E4EA1.scalable_dimension}", + "service_namespace": "\${aws_appautoscaling_target.testPocketApp_autoscaling_autoscaling_target_505E4EA1.service_namespace}", + "step_scaling_policy_configuration": { + "adjustment_type": "ChangeInCapacity", + "cooldown": 60, + "metric_aggregation_type": "Average", + "step_adjustment": [ + { + "metric_interval_upper_bound": "0", + "scaling_adjustment": -1 + } + ] + }, + "target_tracking_scaling_policy_configuration": null + }, + "testPocketApp_autoscaling_scale_out_policy_075BFAC4": { + "depends_on": [ + "aws_appautoscaling_target.testPocketApp_autoscaling_autoscaling_target_505E4EA1" + ], + "name": "testapp-ScaleOutPolicy", + "policy_type": "StepScaling", + "resource_id": "\${aws_appautoscaling_target.testPocketApp_autoscaling_autoscaling_target_505E4EA1.resource_id}", + "scalable_dimension": "\${aws_appautoscaling_target.testPocketApp_autoscaling_autoscaling_target_505E4EA1.scalable_dimension}", + "service_namespace": "\${aws_appautoscaling_target.testPocketApp_autoscaling_autoscaling_target_505E4EA1.service_namespace}", + "step_scaling_policy_configuration": { + "adjustment_type": "ChangeInCapacity", + "cooldown": 60, + "metric_aggregation_type": "Average", + "step_adjustment": [ + { + "metric_interval_lower_bound": "0", + "scaling_adjustment": 2 + } + ] + }, + "target_tracking_scaling_policy_configuration": null + } + }, + "aws_appautoscaling_target": { + "testPocketApp_autoscaling_autoscaling_target_505E4EA1": { + "max_capacity": 2, + "min_capacity": 1, + "resource_id": "service/\${aws_ecs_cluster.testPocketApp_ecs_cluster_C3960066.name}/\${aws_ecs_service.testPocketApp_ecs_service_ecs-service_182DEA4C.name}", + "scalable_dimension": "ecs:service:DesiredCount", + "service_namespace": "ecs" + } + }, + "aws_cloudwatch_dashboard": { + "testPocketApp_cloudwatch-dashboard_26E9F70C": { + "dashboard_body": "{\\"widgets\\":[{\\"type\\":\\"metric\\",\\"x\\":0,\\"y\\":0,\\"width\\":12,\\"height\\":6,\\"properties\\":{\\"metrics\\":[[\\"AWS/ApplicationELB\\",\\"HTTPCode_Target_4XX_Count\\",\\"LoadBalancer\\",\\"\${aws_alb.testPocketApp_application_load_balancer_alb_D538DEEE.arn_suffix}\\",{\\"yAxis\\":\\"left\\",\\"color\\":\\"#ff7f0e\\"}],[\\".\\",\\"RequestCount\\",\\".\\",\\".\\",{\\"yAxis\\":\\"right\\",\\"color\\":\\"#1f77b4\\"}],[\\".\\",\\"HTTPCode_Target_5XX_Count\\",\\".\\",\\".\\",{\\"color\\":\\"#d62728\\"}],[\\".\\",\\"HTTPCode_Target_2XX_Count\\",\\".\\",\\".\\",{\\"yAxis\\":\\"right\\",\\"color\\":\\"#2ca02c\\"}]],\\"view\\":\\"timeSeries\\",\\"stacked\\":false,\\"region\\":\\"us-east-1\\",\\"period\\":60,\\"stat\\":\\"Sum\\",\\"title\\":\\"Target Requests\\"}},{\\"type\\":\\"metric\\",\\"x\\":12,\\"y\\":0,\\"width\\":12,\\"height\\":6,\\"properties\\":{\\"metrics\\":[[\\"AWS/ApplicationELB\\",\\"HTTPCode_ELB_4XX_Count\\",\\"LoadBalancer\\",\\"\${aws_alb.testPocketApp_application_load_balancer_alb_D538DEEE.arn_suffix}\\",{\\"yAxis\\":\\"left\\",\\"color\\":\\"#ff7f0e\\"}],[\\".\\",\\"RequestCount\\",\\".\\",\\".\\",{\\"yAxis\\":\\"right\\",\\"color\\":\\"#1f77b4\\"}],[\\".\\",\\"HTTPCode_ELB_5XX_Count\\",\\".\\",\\".\\",{\\"color\\":\\"#d62728\\"}]],\\"view\\":\\"timeSeries\\",\\"stacked\\":false,\\"region\\":\\"us-east-1\\",\\"period\\":60,\\"stat\\":\\"Sum\\",\\"title\\":\\"ALB Requests\\"}},{\\"type\\":\\"metric\\",\\"x\\":12,\\"y\\":6,\\"width\\":12,\\"height\\":6,\\"properties\\":{\\"metrics\\":[[\\"AWS/ApplicationELB\\",\\"TargetResponseTime\\",\\"LoadBalancer\\",\\"\${aws_alb.testPocketApp_application_load_balancer_alb_D538DEEE.arn_suffix}\\",{\\"label\\":\\"Average\\",\\"color\\":\\"#aec7e8\\"}],[\\"...\\",{\\"stat\\":\\"p95\\",\\"label\\":\\"p95\\",\\"color\\":\\"#ffbb78\\"}],[\\"...\\",{\\"stat\\":\\"p99\\",\\"label\\":\\"p99\\",\\"color\\":\\"#98df8a\\"}]],\\"view\\":\\"timeSeries\\",\\"stacked\\":false,\\"region\\":\\"us-east-1\\",\\"stat\\":\\"Average\\",\\"period\\":60}},{\\"type\\":\\"metric\\",\\"x\\":0,\\"y\\":6,\\"width\\":12,\\"height\\":6,\\"properties\\":{\\"metrics\\":[[\\"ECS/ContainerInsights\\",\\"RunningTaskCount\\",\\"ServiceName\\",\\"\${aws_ecs_service.testPocketApp_ecs_service_ecs-service_182DEA4C.name}\\",\\"ClusterName\\",\\"\${aws_ecs_cluster.testPocketApp_ecs_cluster_C3960066.name}\\",{\\"yAxis\\":\\"right\\",\\"color\\":\\"#c49c94\\"}],[\\"AWS/ECS\\",\\"CPUUtilization\\",\\".\\",\\".\\",\\".\\",\\".\\",{\\"color\\":\\"#f7b6d2\\"}],[\\".\\",\\"MemoryUtilization\\",\\".\\",\\".\\",\\".\\",\\".\\",{\\"color\\":\\"#c7c7c7\\"}]],\\"view\\":\\"timeSeries\\",\\"stacked\\":false,\\"region\\":\\"us-east-1\\",\\"stat\\":\\"Average\\",\\"period\\":60,\\"annotations\\":{\\"horizontal\\":[{\\"color\\":\\"#e377c2\\",\\"label\\":\\"CPU scale out\\",\\"value\\":45},{\\"color\\":\\"#c5b0d5\\",\\"label\\":\\"CPU scale in\\",\\"value\\":30}]},\\"title\\":\\"Service Load\\"}}]}", + "dashboard_name": "testapp", + "lifecycle": { + "ignore_changes": [ + "dashboard_body" + ] + } + } + }, + "aws_cloudwatch_metric_alarm": { + "testPocketApp_autoscaling_scale_in_alarm_69B5F00D": { + "alarm_actions": [ + "\${aws_appautoscaling_policy.testPocketApp_autoscaling_scale_in_policy_A163A235.arn}" + ], + "alarm_description": "Alarm to reduce capacity if container CPU is low", + "alarm_name": "testapp Service Low CPU", + "comparison_operator": "LessThanThreshold", + "dimensions": { + "ClusterName": "\${aws_ecs_cluster.testPocketApp_ecs_cluster_C3960066.name}", + "ServiceName": "\${aws_ecs_service.testPocketApp_ecs_service_ecs-service_182DEA4C.name}" + }, + "evaluation_periods": 2, + "metric_name": "CPUUtilization", + "namespace": "AWS/ECS", + "period": 60, + "statistic": "Average", + "threshold": 30, + "treat_missing_data": "notBreaching" + }, + "testPocketApp_autoscaling_scale_out_alarm_4313FBE9": { + "alarm_actions": [ + "\${aws_appautoscaling_policy.testPocketApp_autoscaling_scale_out_policy_075BFAC4.arn}" + ], + "alarm_description": "Alarm to add capacity if container CPU is high", + "alarm_name": "testapp Service High CPU", + "comparison_operator": "GreaterThanThreshold", + "dimensions": { + "ClusterName": "\${aws_ecs_cluster.testPocketApp_ecs_cluster_C3960066.name}", + "ServiceName": "\${aws_ecs_service.testPocketApp_ecs_service_ecs-service_182DEA4C.name}" + }, + "evaluation_periods": 2, + "metric_name": "CPUUtilization", + "namespace": "AWS/ECS", + "period": 60, + "statistic": "Average", + "threshold": 45, + "treat_missing_data": "notBreaching" + } + }, + "aws_ecs_cluster": { + "testPocketApp_ecs_cluster_C3960066": { + "name": "testapp", + "setting": [ + { + "name": "containerInsights", + "value": "enabled" + } + ] + } + }, + "aws_ecs_service": { + "testPocketApp_ecs_service_ecs-service_182DEA4C": { + "cluster": "\${aws_ecs_cluster.testPocketApp_ecs_cluster_C3960066.arn}", + "depends_on": [ + "aws_alb_target_group.testPocketApp_ecs_service_blue_target_group_ecs_target_group_A4B85885", + "aws_alb_listener_rule.testPocketApp_ecs_service_listener_rule_C03BB04D" + ], + "deployment_controller": { + "type": "ECS" + }, + "deployment_maximum_percent": 200, + "deployment_minimum_healthy_percent": 100, + "desired_count": 2, + "launch_type": "FARGATE", + "lifecycle": { + "ignore_changes": [ + "desired_count", + "load_balancer" + ] + }, + "load_balancer": [ + { + "container_name": "main_container", + "container_port": 8675309, + "target_group_arn": "\${aws_alb_target_group.testPocketApp_ecs_service_blue_target_group_ecs_target_group_A4B85885.arn}" + } + ], + "name": "testapp", + "network_configuration": { + "security_groups": [ + "\${aws_security_group.testPocketApp_ecs_service_ecs_security_group_9C016FA8.id}" + ], + "subnets": "\${data.aws_subnets.testPocketApp_pocket_vpc_private_subnet_ids_EB1E3A65.ids}" + }, + "propagate_tags": "SERVICE", + "task_definition": "\${aws_ecs_task_definition.testPocketApp_ecs_service_ecs-task_A7E74E45.arn}" + } + }, + "aws_ecs_task_definition": { + "testPocketApp_ecs_service_ecs-task_A7E74E45": { + "container_definitions": "[]", + "cpu": "512", + "depends_on": [ + ], + "execution_role_arn": "\${aws_iam_role.testPocketApp_ecs_service_ecs-iam_ecs-execution-role_EB461A0C.arn}", + "family": "testapp", + "memory": "2048", + "network_mode": "awsvpc", + "requires_compatibilities": [ + "FARGATE" + ], + "task_role_arn": "\${aws_iam_role.testPocketApp_ecs_service_ecs-iam_ecs-task-role_7DB93963.arn}", + "volume": [ + ] + } + }, + "aws_iam_role": { + "testPocketApp_ecs_service_ecs-iam_ecs-execution-role_EB461A0C": { + "assume_role_policy": "\${data.aws_iam_policy_document.testPocketApp_ecs_service_ecs-iam_ecs-task-assume_ABB81680.json}", + "name": "testapp-TaskExecutionRole" + }, + "testPocketApp_ecs_service_ecs-iam_ecs-task-role_7DB93963": { + "assume_role_policy": "\${data.aws_iam_policy_document.testPocketApp_ecs_service_ecs-iam_ecs-task-assume_ABB81680.json}", + "name": "testapp-TaskRole" + } + }, + "aws_route53_record": { + "testPocketApp_alb_certificate_certificate_record_3C49201F": { + "depends_on": [ + "aws_acm_certificate.testPocketApp_alb_certificate_417C14FF" + ], + "name": "\${tolist(aws_acm_certificate.testPocketApp_alb_certificate_417C14FF.domain_validation_options).0.resource_record_name}", + "records": [ + "\${tolist(aws_acm_certificate.testPocketApp_alb_certificate_417C14FF.domain_validation_options).0.resource_record_value}" + ], + "ttl": 60, + "type": "\${tolist(aws_acm_certificate.testPocketApp_alb_certificate_417C14FF.domain_validation_options).0.resource_record_type}", + "zone_id": "\${aws_route53_zone.testPocketApp_base_dns_subhosted_zone_1E1E3243.zone_id}" + }, + "testPocketApp_alb_record_7B56ED33": { + "alias": { + "evaluate_target_health": true, + "name": "\${aws_alb.testPocketApp_application_load_balancer_alb_D538DEEE.dns_name}", + "zone_id": "\${aws_alb.testPocketApp_application_load_balancer_alb_D538DEEE.zone_id}" + }, + "lifecycle": { + "ignore_changes": [ + "weighted_routing_policy[0].weight" + ] + }, + "name": "testing.bowling.gov", + "set_identifier": "1", + "type": "A", + "weighted_routing_policy": { + "weight": 1 + }, + "zone_id": "\${aws_route53_zone.testPocketApp_base_dns_subhosted_zone_1E1E3243.zone_id}" + }, + "testPocketApp_base_dns_subhosted_zone_ns_497FFB83": { + "name": "testing.bowling.gov", + "records": "\${aws_route53_zone.testPocketApp_base_dns_subhosted_zone_1E1E3243.name_servers}", + "ttl": 86400, + "type": "NS", + "zone_id": "\${data.aws_route53_zone.testPocketApp_base_dns_main_hosted_zone_9442EEB0.zone_id}" + } + }, + "aws_route53_zone": { + "testPocketApp_base_dns_subhosted_zone_1E1E3243": { + "name": "testing.bowling.gov" + } + }, + "aws_security_group": { + "testPocketApp_application_load_balancer_alb_security_group_91A27046": { + "description": "External security group (Managed by Terraform)", + "egress": [ + { + "cidr_blocks": [ + "0.0.0.0/0" + ], + "description": "required", + "from_port": 0, + "ipv6_cidr_blocks": [ + ], + "prefix_list_ids": [ + ], + "protocol": "-1", + "security_groups": [ + ], + "self": null, + "to_port": 0 + } + ], + "ingress": [ + { + "cidr_blocks": [ + "0.0.0.0/0" + ], + "description": null, + "from_port": 443, + "ipv6_cidr_blocks": null, + "prefix_list_ids": null, + "protocol": "TCP", + "security_groups": null, + "self": null, + "to_port": 443 + }, + { + "cidr_blocks": [ + "0.0.0.0/0" + ], + "description": null, + "from_port": 80, + "ipv6_cidr_blocks": null, + "prefix_list_ids": null, + "protocol": "TCP", + "security_groups": null, + "self": null, + "to_port": 80 + } + ], + "lifecycle": { + "create_before_destroy": true + }, + "name_prefix": "testapp-HTTP/S Security Group", + "tags": { + "Name": "testapp-HTTP/S Security Group" + }, + "vpc_id": "\${data.aws_vpc.testPocketApp_pocket_vpc_C4E157E3.id}" + }, + "testPocketApp_ecs_service_ecs_security_group_9C016FA8": { + "description": "Internal ECS Security Group (Managed by Terraform)", + "egress": [ + { + "cidr_blocks": [ + "0.0.0.0/0" + ], + "description": "required", + "from_port": 0, + "ipv6_cidr_blocks": [ + ], + "prefix_list_ids": [ + ], + "protocol": "-1", + "security_groups": [ + ], + "self": null, + "to_port": 0 + } + ], + "ingress": [ + { + "cidr_blocks": [ + ], + "description": "required", + "from_port": 80, + "ipv6_cidr_blocks": [ + ], + "prefix_list_ids": [ + ], + "protocol": "TCP", + "security_groups": [ + "\${aws_security_group.testPocketApp_application_load_balancer_alb_security_group_91A27046.id}" + ], + "self": null, + "to_port": 8675309 + } + ], + "lifecycle": { + "create_before_destroy": true + }, + "name_prefix": "testapp-ECSSecurityGroup", + "vpc_id": "\${data.aws_vpc.testPocketApp_pocket_vpc_C4E157E3.id}" + } + } + } +}" +`; + +exports[`PocketALBApplication renders an internal application with tags 1`] = ` +"{ + "data": { + "aws_caller_identity": { + "testPocketApp_pocket_vpc_current_identity_06D87057": { + } + }, + "aws_iam_policy_document": { + "testPocketApp_ecs_service_ecs-iam_ecs-task-assume_ABB81680": { + "statement": [ + { + "actions": [ + "sts:AssumeRole" + ], + "effect": "Allow", + "principals": [ + { + "identifiers": [ + "ecs-tasks.amazonaws.com" + ], + "type": "Service" + } + ] + } + ], + "version": "2012-10-17" + } + }, + "aws_kms_alias": { + "testPocketApp_pocket_vpc_secrets_manager_key_9FD2BC93": { + "name": "alias/aws/secretsmanager" + } + }, + "aws_region": { + "testPocketApp_pocket_vpc_current_region_8ED435E7": { + } + }, + "aws_route53_zone": { + "testPocketApp_base_dns_main_hosted_zone_9442EEB0": { + "name": "bowling.gov" + } + }, + "aws_security_groups": { + "testPocketApp_pocket_vpc_default_security_groups_40B4FC48": { + "filter": [ + { + "name": "group-name", + "values": [ + "default" + ] + }, + { + "name": "vpc-id", + "values": [ + "\${data.aws_vpc.testPocketApp_pocket_vpc_C4E157E3.id}" + ] + } + ] + }, + "testPocketApp_pocket_vpc_internal_security_groups_12F7F666": { + "filter": [ + { + "name": "group-name", + "values": [ + "pocket-vpc-internal" + ] + }, + { + "name": "vpc-id", + "values": [ + "\${data.aws_vpc.testPocketApp_pocket_vpc_C4E157E3.id}" + ] + } + ] + } + }, + "aws_ssm_parameter": { + "testPocketApp_pocket_vpc_private_subnets_9D449563": { + "name": "/Shared/PrivateSubnets" + }, + "testPocketApp_pocket_vpc_public_subnets_B0B3A6AB": { + "name": "/Shared/PublicSubnets" + }, + "testPocketApp_pocket_vpc_vpc_ssm_param_235525D4": { + "name": "/Shared/Vpc" + } + }, + "aws_subnets": { + "testPocketApp_pocket_vpc_private_subnet_ids_EB1E3A65": { + "filter": [ + { + "name": "subnet-id", + "values": "\${split(\\",\\", data.aws_ssm_parameter.testPocketApp_pocket_vpc_private_subnets_9D449563.value)}" + }, + { + "name": "vpc-id", + "values": [ + "\${data.aws_vpc.testPocketApp_pocket_vpc_C4E157E3.id}" + ] + } + ] + }, + "testPocketApp_pocket_vpc_public_subnet_ids_01F0B902": { + "filter": [ + { + "name": "subnet-id", + "values": "\${split(\\",\\", data.aws_ssm_parameter.testPocketApp_pocket_vpc_public_subnets_B0B3A6AB.value)}" + }, + { + "name": "vpc-id", + "values": [ + "\${data.aws_vpc.testPocketApp_pocket_vpc_C4E157E3.id}" + ] + } + ] + } + }, + "aws_vpc": { + "testPocketApp_pocket_vpc_C4E157E3": { + "filter": [ + { + "name": "vpc-id", + "values": [ + "\${data.aws_ssm_parameter.testPocketApp_pocket_vpc_vpc_ssm_param_235525D4.value}" + ] + } + ] + } + } + }, + "resource": { + "aws_acm_certificate": { + "testPocketApp_alb_certificate_417C14FF": { + "domain_name": "testing.bowling.gov", + "lifecycle": { + "create_before_destroy": true + }, + "tags": { + "hobby": "bowling", + "name": "thedude" + }, + "validation_method": "DNS" + } + }, + "aws_acm_certificate_validation": { + "testPocketApp_alb_certificate_certificate_validation_7D899C6A": { + "certificate_arn": "\${aws_acm_certificate.testPocketApp_alb_certificate_417C14FF.arn}", + "depends_on": [ + "aws_route53_record.testPocketApp_alb_certificate_certificate_record_3C49201F", + "aws_acm_certificate.testPocketApp_alb_certificate_417C14FF" + ], + "validation_record_fqdns": [ + "\${aws_route53_record.testPocketApp_alb_certificate_certificate_record_3C49201F.fqdn}" + ] + } + }, + "aws_alb": { + "testPocketApp_application_load_balancer_alb_D538DEEE": { + "internal": true, + "name_prefix": "TSTAPP", + "security_groups": [ + "\${aws_security_group.testPocketApp_application_load_balancer_alb_security_group_91A27046.id}" + ], + "subnets": "\${data.aws_subnets.testPocketApp_pocket_vpc_private_subnet_ids_EB1E3A65.ids}", + "tags": { + "hobby": "bowling", + "name": "thedude" + } + } + }, + "aws_alb_listener": { + "testPocketApp_listener_http_A9E227C2": { + "default_action": [ + { + "redirect": { + "port": "443", + "protocol": "HTTPS", + "status_code": "HTTP_301" + }, + "type": "redirect" + } + ], + "load_balancer_arn": "\${aws_alb.testPocketApp_application_load_balancer_alb_D538DEEE.arn}", + "port": 80, + "protocol": "HTTP", + "tags": { + "hobby": "bowling", + "name": "thedude" + } + }, + "testPocketApp_listener_https_7F49DE83": { + "certificate_arn": "\${aws_acm_certificate.testPocketApp_alb_certificate_417C14FF.arn}", + "default_action": [ + { + "fixed_response": { + "content_type": "text/plain", + "message_body": "", + "status_code": "503" + }, + "type": "fixed-response" + } + ], + "load_balancer_arn": "\${aws_alb.testPocketApp_application_load_balancer_alb_D538DEEE.arn}", + "port": 443, + "protocol": "HTTPS", + "ssl_policy": "ELBSecurityPolicy-TLS-1-1-2017-01", + "tags": { + "hobby": "bowling", + "name": "thedude" + } + } + }, + "aws_alb_listener_rule": { + "testPocketApp_ecs_service_listener_rule_C03BB04D": { + "action": [ + { + "target_group_arn": "\${aws_alb_target_group.testPocketApp_ecs_service_blue_target_group_ecs_target_group_A4B85885.arn}", + "type": "forward" + } + ], + "condition": [ + { + "path_pattern": { + "values": [ + "*" + ] + } + } + ], + "lifecycle": { + "ignore_changes": [ + "action" + ] + }, + "listener_arn": "\${aws_alb_listener.testPocketApp_listener_https_7F49DE83.arn}", + "priority": 1, + "tags": { + "hobby": "bowling", + "name": "thedude" + } + } + }, + "aws_alb_target_group": { + "testPocketApp_ecs_service_blue_target_group_ecs_target_group_A4B85885": { + "deregistration_delay": "120", + "health_check": { + "healthy_threshold": 5, + "interval": 15, + "path": "/test", + "protocol": "HTTP", + "unhealthy_threshold": 3 + }, + "name_prefix": "TSTAPP", + "port": 80, + "protocol": "HTTP", + "tags": { + "hobby": "bowling", + "name": "thedude", + "type": "blue" + }, + "target_type": "ip", + "vpc_id": "\${data.aws_vpc.testPocketApp_pocket_vpc_C4E157E3.id}" + } + }, + "aws_appautoscaling_policy": { + "testPocketApp_autoscaling_scale_in_policy_A163A235": { + "depends_on": [ + "aws_appautoscaling_target.testPocketApp_autoscaling_autoscaling_target_505E4EA1" + ], + "name": "testapp-ScaleInPolicy", + "policy_type": "StepScaling", + "resource_id": "\${aws_appautoscaling_target.testPocketApp_autoscaling_autoscaling_target_505E4EA1.resource_id}", + "scalable_dimension": "\${aws_appautoscaling_target.testPocketApp_autoscaling_autoscaling_target_505E4EA1.scalable_dimension}", + "service_namespace": "\${aws_appautoscaling_target.testPocketApp_autoscaling_autoscaling_target_505E4EA1.service_namespace}", + "step_scaling_policy_configuration": { + "adjustment_type": "ChangeInCapacity", + "cooldown": 60, + "metric_aggregation_type": "Average", + "step_adjustment": [ + { + "metric_interval_upper_bound": "0", + "scaling_adjustment": -1 + } + ] + }, + "target_tracking_scaling_policy_configuration": null + }, + "testPocketApp_autoscaling_scale_out_policy_075BFAC4": { + "depends_on": [ + "aws_appautoscaling_target.testPocketApp_autoscaling_autoscaling_target_505E4EA1" + ], + "name": "testapp-ScaleOutPolicy", + "policy_type": "StepScaling", + "resource_id": "\${aws_appautoscaling_target.testPocketApp_autoscaling_autoscaling_target_505E4EA1.resource_id}", + "scalable_dimension": "\${aws_appautoscaling_target.testPocketApp_autoscaling_autoscaling_target_505E4EA1.scalable_dimension}", + "service_namespace": "\${aws_appautoscaling_target.testPocketApp_autoscaling_autoscaling_target_505E4EA1.service_namespace}", + "step_scaling_policy_configuration": { + "adjustment_type": "ChangeInCapacity", + "cooldown": 60, + "metric_aggregation_type": "Average", + "step_adjustment": [ + { + "metric_interval_lower_bound": "0", + "scaling_adjustment": 2 + } + ] + }, + "target_tracking_scaling_policy_configuration": null + } + }, + "aws_appautoscaling_target": { + "testPocketApp_autoscaling_autoscaling_target_505E4EA1": { + "max_capacity": 2, + "min_capacity": 1, + "resource_id": "service/\${aws_ecs_cluster.testPocketApp_ecs_cluster_C3960066.name}/\${aws_ecs_service.testPocketApp_ecs_service_ecs-service_182DEA4C.name}", + "scalable_dimension": "ecs:service:DesiredCount", + "service_namespace": "ecs" + } + }, + "aws_cloudwatch_dashboard": { + "testPocketApp_cloudwatch-dashboard_26E9F70C": { + "dashboard_body": "{\\"widgets\\":[{\\"type\\":\\"metric\\",\\"x\\":0,\\"y\\":0,\\"width\\":12,\\"height\\":6,\\"properties\\":{\\"metrics\\":[[\\"AWS/ApplicationELB\\",\\"HTTPCode_Target_4XX_Count\\",\\"LoadBalancer\\",\\"\${aws_alb.testPocketApp_application_load_balancer_alb_D538DEEE.arn_suffix}\\",{\\"yAxis\\":\\"left\\",\\"color\\":\\"#ff7f0e\\"}],[\\".\\",\\"RequestCount\\",\\".\\",\\".\\",{\\"yAxis\\":\\"right\\",\\"color\\":\\"#1f77b4\\"}],[\\".\\",\\"HTTPCode_Target_5XX_Count\\",\\".\\",\\".\\",{\\"color\\":\\"#d62728\\"}],[\\".\\",\\"HTTPCode_Target_2XX_Count\\",\\".\\",\\".\\",{\\"yAxis\\":\\"right\\",\\"color\\":\\"#2ca02c\\"}]],\\"view\\":\\"timeSeries\\",\\"stacked\\":false,\\"region\\":\\"us-east-1\\",\\"period\\":60,\\"stat\\":\\"Sum\\",\\"title\\":\\"Target Requests\\"}},{\\"type\\":\\"metric\\",\\"x\\":12,\\"y\\":0,\\"width\\":12,\\"height\\":6,\\"properties\\":{\\"metrics\\":[[\\"AWS/ApplicationELB\\",\\"HTTPCode_ELB_4XX_Count\\",\\"LoadBalancer\\",\\"\${aws_alb.testPocketApp_application_load_balancer_alb_D538DEEE.arn_suffix}\\",{\\"yAxis\\":\\"left\\",\\"color\\":\\"#ff7f0e\\"}],[\\".\\",\\"RequestCount\\",\\".\\",\\".\\",{\\"yAxis\\":\\"right\\",\\"color\\":\\"#1f77b4\\"}],[\\".\\",\\"HTTPCode_ELB_5XX_Count\\",\\".\\",\\".\\",{\\"color\\":\\"#d62728\\"}]],\\"view\\":\\"timeSeries\\",\\"stacked\\":false,\\"region\\":\\"us-east-1\\",\\"period\\":60,\\"stat\\":\\"Sum\\",\\"title\\":\\"ALB Requests\\"}},{\\"type\\":\\"metric\\",\\"x\\":12,\\"y\\":6,\\"width\\":12,\\"height\\":6,\\"properties\\":{\\"metrics\\":[[\\"AWS/ApplicationELB\\",\\"TargetResponseTime\\",\\"LoadBalancer\\",\\"\${aws_alb.testPocketApp_application_load_balancer_alb_D538DEEE.arn_suffix}\\",{\\"label\\":\\"Average\\",\\"color\\":\\"#aec7e8\\"}],[\\"...\\",{\\"stat\\":\\"p95\\",\\"label\\":\\"p95\\",\\"color\\":\\"#ffbb78\\"}],[\\"...\\",{\\"stat\\":\\"p99\\",\\"label\\":\\"p99\\",\\"color\\":\\"#98df8a\\"}]],\\"view\\":\\"timeSeries\\",\\"stacked\\":false,\\"region\\":\\"us-east-1\\",\\"stat\\":\\"Average\\",\\"period\\":60}},{\\"type\\":\\"metric\\",\\"x\\":0,\\"y\\":6,\\"width\\":12,\\"height\\":6,\\"properties\\":{\\"metrics\\":[[\\"ECS/ContainerInsights\\",\\"RunningTaskCount\\",\\"ServiceName\\",\\"\${aws_ecs_service.testPocketApp_ecs_service_ecs-service_182DEA4C.name}\\",\\"ClusterName\\",\\"\${aws_ecs_cluster.testPocketApp_ecs_cluster_C3960066.name}\\",{\\"yAxis\\":\\"right\\",\\"color\\":\\"#c49c94\\"}],[\\"AWS/ECS\\",\\"CPUUtilization\\",\\".\\",\\".\\",\\".\\",\\".\\",{\\"color\\":\\"#f7b6d2\\"}],[\\".\\",\\"MemoryUtilization\\",\\".\\",\\".\\",\\".\\",\\".\\",{\\"color\\":\\"#c7c7c7\\"}]],\\"view\\":\\"timeSeries\\",\\"stacked\\":false,\\"region\\":\\"us-east-1\\",\\"stat\\":\\"Average\\",\\"period\\":60,\\"annotations\\":{\\"horizontal\\":[{\\"color\\":\\"#e377c2\\",\\"label\\":\\"CPU scale out\\",\\"value\\":45},{\\"color\\":\\"#c5b0d5\\",\\"label\\":\\"CPU scale in\\",\\"value\\":30}]},\\"title\\":\\"Service Load\\"}}]}", + "dashboard_name": "testapp", + "lifecycle": { + "ignore_changes": [ + "dashboard_body" + ] + } + } + }, + "aws_cloudwatch_metric_alarm": { + "testPocketApp_autoscaling_scale_in_alarm_69B5F00D": { + "alarm_actions": [ + "\${aws_appautoscaling_policy.testPocketApp_autoscaling_scale_in_policy_A163A235.arn}" + ], + "alarm_description": "Alarm to reduce capacity if container CPU is low", + "alarm_name": "testapp Service Low CPU", + "comparison_operator": "LessThanThreshold", + "dimensions": { + "ClusterName": "\${aws_ecs_cluster.testPocketApp_ecs_cluster_C3960066.name}", + "ServiceName": "\${aws_ecs_service.testPocketApp_ecs_service_ecs-service_182DEA4C.name}" + }, + "evaluation_periods": 2, + "metric_name": "CPUUtilization", + "namespace": "AWS/ECS", + "period": 60, + "statistic": "Average", + "tags": { + "hobby": "bowling", + "name": "thedude" + }, + "threshold": 30, + "treat_missing_data": "notBreaching" + }, + "testPocketApp_autoscaling_scale_out_alarm_4313FBE9": { + "alarm_actions": [ + "\${aws_appautoscaling_policy.testPocketApp_autoscaling_scale_out_policy_075BFAC4.arn}" + ], + "alarm_description": "Alarm to add capacity if container CPU is high", + "alarm_name": "testapp Service High CPU", + "comparison_operator": "GreaterThanThreshold", + "dimensions": { + "ClusterName": "\${aws_ecs_cluster.testPocketApp_ecs_cluster_C3960066.name}", + "ServiceName": "\${aws_ecs_service.testPocketApp_ecs_service_ecs-service_182DEA4C.name}" + }, + "evaluation_periods": 2, + "metric_name": "CPUUtilization", + "namespace": "AWS/ECS", + "period": 60, + "statistic": "Average", + "tags": { + "hobby": "bowling", + "name": "thedude" + }, + "threshold": 45, + "treat_missing_data": "notBreaching" + } + }, + "aws_ecs_cluster": { + "testPocketApp_ecs_cluster_C3960066": { + "name": "testapp", + "setting": [ + { + "name": "containerInsights", + "value": "enabled" + } + ], + "tags": { + "hobby": "bowling", + "name": "thedude" + } + } + }, + "aws_ecs_service": { + "testPocketApp_ecs_service_ecs-service_182DEA4C": { + "cluster": "\${aws_ecs_cluster.testPocketApp_ecs_cluster_C3960066.arn}", + "depends_on": [ + "aws_alb_target_group.testPocketApp_ecs_service_blue_target_group_ecs_target_group_A4B85885", + "aws_alb_listener_rule.testPocketApp_ecs_service_listener_rule_C03BB04D" + ], + "deployment_controller": { + "type": "ECS" + }, + "deployment_maximum_percent": 200, + "deployment_minimum_healthy_percent": 100, + "desired_count": 2, + "launch_type": "FARGATE", + "lifecycle": { + "ignore_changes": [ + "desired_count", + "load_balancer" + ] + }, + "load_balancer": [ + { + "container_name": "main_container", + "container_port": 8675309, + "target_group_arn": "\${aws_alb_target_group.testPocketApp_ecs_service_blue_target_group_ecs_target_group_A4B85885.arn}" + } + ], + "name": "testapp", + "network_configuration": { + "security_groups": [ + "\${aws_security_group.testPocketApp_ecs_service_ecs_security_group_9C016FA8.id}" + ], + "subnets": "\${data.aws_subnets.testPocketApp_pocket_vpc_private_subnet_ids_EB1E3A65.ids}" + }, + "propagate_tags": "SERVICE", + "tags": { + "hobby": "bowling", + "name": "thedude" + }, + "task_definition": "\${aws_ecs_task_definition.testPocketApp_ecs_service_ecs-task_A7E74E45.arn}" + } + }, + "aws_ecs_task_definition": { + "testPocketApp_ecs_service_ecs-task_A7E74E45": { + "container_definitions": "[]", + "cpu": "512", + "depends_on": [ + ], + "execution_role_arn": "\${aws_iam_role.testPocketApp_ecs_service_ecs-iam_ecs-execution-role_EB461A0C.arn}", + "family": "testapp", + "memory": "2048", + "network_mode": "awsvpc", + "requires_compatibilities": [ + "FARGATE" + ], + "tags": { + "hobby": "bowling", + "name": "thedude" + }, + "task_role_arn": "\${aws_iam_role.testPocketApp_ecs_service_ecs-iam_ecs-task-role_7DB93963.arn}", + "volume": [ + ] + } + }, + "aws_iam_role": { + "testPocketApp_ecs_service_ecs-iam_ecs-execution-role_EB461A0C": { + "assume_role_policy": "\${data.aws_iam_policy_document.testPocketApp_ecs_service_ecs-iam_ecs-task-assume_ABB81680.json}", + "name": "testapp-TaskExecutionRole", + "tags": { + "hobby": "bowling", + "name": "thedude" + } + }, + "testPocketApp_ecs_service_ecs-iam_ecs-task-role_7DB93963": { + "assume_role_policy": "\${data.aws_iam_policy_document.testPocketApp_ecs_service_ecs-iam_ecs-task-assume_ABB81680.json}", + "name": "testapp-TaskRole", + "tags": { + "hobby": "bowling", + "name": "thedude" + } + } + }, + "aws_route53_record": { + "testPocketApp_alb_certificate_certificate_record_3C49201F": { + "depends_on": [ + "aws_acm_certificate.testPocketApp_alb_certificate_417C14FF" + ], + "name": "\${tolist(aws_acm_certificate.testPocketApp_alb_certificate_417C14FF.domain_validation_options).0.resource_record_name}", + "records": [ + "\${tolist(aws_acm_certificate.testPocketApp_alb_certificate_417C14FF.domain_validation_options).0.resource_record_value}" + ], + "ttl": 60, + "type": "\${tolist(aws_acm_certificate.testPocketApp_alb_certificate_417C14FF.domain_validation_options).0.resource_record_type}", + "zone_id": "\${aws_route53_zone.testPocketApp_base_dns_subhosted_zone_1E1E3243.zone_id}" + }, + "testPocketApp_alb_record_7B56ED33": { + "alias": { + "evaluate_target_health": true, + "name": "\${aws_alb.testPocketApp_application_load_balancer_alb_D538DEEE.dns_name}", + "zone_id": "\${aws_alb.testPocketApp_application_load_balancer_alb_D538DEEE.zone_id}" + }, + "lifecycle": { + "ignore_changes": [ + "weighted_routing_policy[0].weight" + ] + }, + "name": "testing.bowling.gov", + "set_identifier": "1", + "type": "A", + "weighted_routing_policy": { + "weight": 1 + }, + "zone_id": "\${aws_route53_zone.testPocketApp_base_dns_subhosted_zone_1E1E3243.zone_id}" + }, + "testPocketApp_base_dns_subhosted_zone_ns_497FFB83": { + "name": "testing.bowling.gov", + "records": "\${aws_route53_zone.testPocketApp_base_dns_subhosted_zone_1E1E3243.name_servers}", + "ttl": 86400, + "type": "NS", + "zone_id": "\${data.aws_route53_zone.testPocketApp_base_dns_main_hosted_zone_9442EEB0.zone_id}" + } + }, + "aws_route53_zone": { + "testPocketApp_base_dns_subhosted_zone_1E1E3243": { + "name": "testing.bowling.gov", + "tags": { + "hobby": "bowling", + "name": "thedude" + } + } + }, + "aws_security_group": { + "testPocketApp_application_load_balancer_alb_security_group_91A27046": { + "description": "External security group (Managed by Terraform)", + "egress": [ + { + "cidr_blocks": [ + "0.0.0.0/0" + ], + "description": "required", + "from_port": 0, + "ipv6_cidr_blocks": [ + ], + "prefix_list_ids": [ + ], + "protocol": "-1", + "security_groups": [ + ], + "self": null, + "to_port": 0 + } + ], + "ingress": [ + { + "cidr_blocks": [ + "0.0.0.0/0" + ], + "description": null, + "from_port": 443, + "ipv6_cidr_blocks": null, + "prefix_list_ids": null, + "protocol": "TCP", + "security_groups": null, + "self": null, + "to_port": 443 + }, + { + "cidr_blocks": [ + "0.0.0.0/0" + ], + "description": null, + "from_port": 80, + "ipv6_cidr_blocks": null, + "prefix_list_ids": null, + "protocol": "TCP", + "security_groups": null, + "self": null, + "to_port": 80 + } + ], + "lifecycle": { + "create_before_destroy": true + }, + "name_prefix": "testapp-HTTP/S Security Group", + "tags": { + "Name": "testapp-HTTP/S Security Group", + "hobby": "bowling", + "name": "thedude" + }, + "vpc_id": "\${data.aws_vpc.testPocketApp_pocket_vpc_C4E157E3.id}" + }, + "testPocketApp_ecs_service_ecs_security_group_9C016FA8": { + "description": "Internal ECS Security Group (Managed by Terraform)", + "egress": [ + { + "cidr_blocks": [ + "0.0.0.0/0" + ], + "description": "required", + "from_port": 0, + "ipv6_cidr_blocks": [ + ], + "prefix_list_ids": [ + ], + "protocol": "-1", + "security_groups": [ + ], + "self": null, + "to_port": 0 + } + ], + "ingress": [ + { + "cidr_blocks": [ + ], + "description": "required", + "from_port": 80, + "ipv6_cidr_blocks": [ + ], + "prefix_list_ids": [ + ], + "protocol": "TCP", + "security_groups": [ + "\${aws_security_group.testPocketApp_application_load_balancer_alb_security_group_91A27046.id}" + ], + "self": null, + "to_port": 8675309 + } + ], + "lifecycle": { + "create_before_destroy": true + }, + "name_prefix": "testapp-ECSSecurityGroup", + "tags": { + "hobby": "bowling", + "name": "thedude" + }, + "vpc_id": "\${data.aws_vpc.testPocketApp_pocket_vpc_C4E157E3.id}" + } + } + } +}" +`; diff --git a/packages/terraform-modules/src/pocket/__snapshots__/PocketApiGatewayLambdaIntegration.spec.ts.snap b/packages/terraform-modules/src/pocket/__snapshots__/PocketApiGatewayLambdaIntegration.spec.ts.snap new file mode 100644 index 00000000..17450934 --- /dev/null +++ b/packages/terraform-modules/src/pocket/__snapshots__/PocketApiGatewayLambdaIntegration.spec.ts.snap @@ -0,0 +1,303 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`PocketApiGatewayLambdaIntegration renders an api gateway with a lambda integration 1`] = ` +"{ + "data": { + "archive_file": { + "test-api-lambda_endpoint-lambda_lambda-default-file_8FD40136": { + "output_path": "index.py.zip", + "source": [ + { + "content": "import json\\ndef handler(event, context):\\n\\t print(event)\\n\\t return {'statusCode': 200, 'headers': {'dance': 'party'}, 'body': json.dumps({'electric': 'boogaloo'}), 'isBase64Encoded': False}", + "filename": "index.py" + } + ], + "type": "zip" + } + }, + "aws_iam_policy_document": { + "test-api-lambda_endpoint-lambda_assume-policy-document_345E1069": { + "statement": [ + { + "actions": [ + "sts:AssumeRole" + ], + "effect": "Allow", + "principals": [ + { + "identifiers": [ + "lambda.amazonaws.com", + "edgelambda.amazonaws.com" + ], + "type": "Service" + } + ] + } + ], + "version": "2012-10-17" + }, + "test-api-lambda_endpoint-lambda_execution-policy-document_4437ECD0": { + "statement": [ + { + "actions": [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents", + "logs:DescribeLogStreams" + ], + "effect": "Allow", + "resources": [ + "arn:aws:logs:*:*:*" + ] + } + ], + "version": "2012-10-17" + } + }, + "aws_route53_zone": { + "test-api-lambda_base-dns_main_hosted_zone_5F97294A": { + "name": "getpocket.dev" + } + } + }, + "resource": { + "aws_acm_certificate": { + "test-api-lambda_api-gateway-certificate_8D77B940": { + "domain_name": "exampleapi.getpocket.dev", + "lifecycle": { + "create_before_destroy": true + }, + "validation_method": "DNS" + } + }, + "aws_acm_certificate_validation": { + "test-api-lambda_api-gateway-certificate_certificate_validation_6C09E593": { + "certificate_arn": "\${aws_acm_certificate.test-api-lambda_api-gateway-certificate_8D77B940.arn}", + "depends_on": [ + "aws_route53_record.test-api-lambda_api-gateway-certificate_certificate_record_BED7C8FC", + "aws_acm_certificate.test-api-lambda_api-gateway-certificate_8D77B940" + ], + "validation_record_fqdns": [ + "\${aws_route53_record.test-api-lambda_api-gateway-certificate_certificate_record_BED7C8FC.fqdn}" + ] + } + }, + "aws_api_gateway_base_path_mapping": { + "test-api-lambda_api-gateway-base-path-mapping_CC4D8627": { + "api_id": "\${aws_api_gateway_rest_api.api-gateway-rest.id}", + "base_path": "fxaProxy", + "domain_name": "\${aws_api_gateway_domain_name.test-api-lambda_api-gateway-domain-name_6DBA5B05.domain_name}", + "stage_name": "\${aws_api_gateway_stage.api-gateway-stage.stage_name}" + } + }, + "aws_api_gateway_deployment": { + "api-gateway-deployment": { + "depends_on": [ + "aws_api_gateway_integration.test-api-lambda_endpoint-integration_8D80A8F8", + "aws_api_gateway_method.test-api-lambda_endpoint-method_B2F73D1A", + "aws_api_gateway_resource.test-api-lambda_endpoint_02B05EB1", + "aws_lambda_alias.test-api-lambda_endpoint-lambda_alias_A14F4FF8" + ], + "lifecycle": { + "create_before_destroy": true + }, + "rest_api_id": "\${aws_api_gateway_rest_api.api-gateway-rest.id}", + "triggers": { + "deployedAt": "1637693316456", + "redeployment": "\${sha1(jsonencode({\\"resources\\" = [aws_api_gateway_integration.test-api-lambda_endpoint-integration_8D80A8F8.id, aws_api_gateway_method.test-api-lambda_endpoint-method_B2F73D1A.id, aws_api_gateway_resource.test-api-lambda_endpoint_02B05EB1.id, aws_lambda_alias.test-api-lambda_endpoint-lambda_alias_A14F4FF8.id]}))}" + } + } + }, + "aws_api_gateway_domain_name": { + "test-api-lambda_api-gateway-domain-name_6DBA5B05": { + "certificate_arn": "\${aws_acm_certificate.test-api-lambda_api-gateway-certificate_8D77B940.arn}", + "depends_on": [ + "aws_acm_certificate_validation.test-api-lambda_api-gateway-certificate_certificate_validation_6C09E593" + ], + "domain_name": "exampleapi.getpocket.dev" + } + }, + "aws_api_gateway_integration": { + "test-api-lambda_endpoint-integration_8D80A8F8": { + "http_method": "POST", + "integration_http_method": "POST", + "resource_id": "\${aws_api_gateway_resource.test-api-lambda_endpoint_02B05EB1.id}", + "rest_api_id": "\${aws_api_gateway_rest_api.api-gateway-rest.id}", + "type": "AWS_PROXY", + "uri": "\${aws_lambda_alias.test-api-lambda_endpoint-lambda_alias_A14F4FF8.invoke_arn}" + } + }, + "aws_api_gateway_method": { + "test-api-lambda_endpoint-method_B2F73D1A": { + "authorization": "NONE", + "http_method": "POST", + "resource_id": "\${aws_api_gateway_resource.test-api-lambda_endpoint_02B05EB1.id}", + "rest_api_id": "\${aws_api_gateway_rest_api.api-gateway-rest.id}" + } + }, + "aws_api_gateway_resource": { + "test-api-lambda_endpoint_02B05EB1": { + "parent_id": "\${aws_api_gateway_rest_api.api-gateway-rest.root_resource_id}", + "path_part": "endpoint", + "rest_api_id": "\${aws_api_gateway_rest_api.api-gateway-rest.id}" + } + }, + "aws_api_gateway_rest_api": { + "api-gateway-rest": { + "name": "test-api-lambda" + } + }, + "aws_api_gateway_stage": { + "api-gateway-stage": { + "deployment_id": "\${aws_api_gateway_deployment.api-gateway-deployment.id}", + "rest_api_id": "\${aws_api_gateway_rest_api.api-gateway-rest.id}", + "stage_name": "test" + } + }, + "aws_cloudwatch_log_group": { + "test-api-lambda_endpoint-lambda_log-group_E143678F": { + "depends_on": [ + "aws_lambda_function.test-api-lambda_endpoint-lambda_3FA91964" + ], + "name": "/aws/lambda/\${aws_lambda_function.test-api-lambda_endpoint-lambda_3FA91964.function_name}", + "retention_in_days": 14 + } + }, + "aws_iam_policy": { + "test-api-lambda_endpoint-lambda_execution-policy_6847E583": { + "name": "lambda-endpoint-ExecutionRolePolicy", + "policy": "\${data.aws_iam_policy_document.test-api-lambda_endpoint-lambda_execution-policy-document_4437ECD0.json}" + } + }, + "aws_iam_role": { + "test-api-lambda_endpoint-lambda_execution-role_46759F27": { + "assume_role_policy": "\${data.aws_iam_policy_document.test-api-lambda_endpoint-lambda_assume-policy-document_345E1069.json}", + "name": "lambda-endpoint-ExecutionRole" + } + }, + "aws_iam_role_policy_attachment": { + "test-api-lambda_endpoint-lambda_execution-role-policy-attachment_5BF51F3C": { + "depends_on": [ + "aws_iam_role.test-api-lambda_endpoint-lambda_execution-role_46759F27", + "aws_iam_policy.test-api-lambda_endpoint-lambda_execution-policy_6847E583" + ], + "policy_arn": "\${aws_iam_policy.test-api-lambda_endpoint-lambda_execution-policy_6847E583.arn}", + "role": "\${aws_iam_role.test-api-lambda_endpoint-lambda_execution-role_46759F27.name}" + } + }, + "aws_lambda_alias": { + "test-api-lambda_endpoint-lambda_alias_A14F4FF8": { + "depends_on": [ + "aws_lambda_function.test-api-lambda_endpoint-lambda_3FA91964" + ], + "function_name": "\${aws_lambda_function.test-api-lambda_endpoint-lambda_3FA91964.function_name}", + "function_version": "\${element(split(\\":\\", aws_lambda_function.test-api-lambda_endpoint-lambda_3FA91964.qualified_arn), 7)}", + "lifecycle": { + "ignore_changes": [ + "function_version" + ] + }, + "name": "DEPLOYED" + } + }, + "aws_lambda_function": { + "test-api-lambda_endpoint-lambda_3FA91964": { + "filename": "\${data.archive_file.test-api-lambda_endpoint-lambda_lambda-default-file_8FD40136.output_path}", + "function_name": "lambda-endpoint-Function", + "handler": "index.handler", + "lifecycle": { + "ignore_changes": [ + "filename", + "source_code_hash" + ] + }, + "memory_size": 128, + "publish": true, + "reserved_concurrent_executions": -1, + "role": "\${aws_iam_role.test-api-lambda_endpoint-lambda_execution-role_46759F27.arn}", + "runtime": "python3.8", + "source_code_hash": "\${data.archive_file.test-api-lambda_endpoint-lambda_lambda-default-file_8FD40136.output_base64sha256}", + "timeout": 5 + } + }, + "aws_lambda_permission": { + "test-api-lambda_test-api-lambda_endpoint-lambda_alias_A14F4FF8-allow-gateway-lambda-invoke_F4009ED6": { + "action": "lambda:InvokeFunction", + "function_name": "\${aws_lambda_alias.test-api-lambda_endpoint-lambda_alias_A14F4FF8.function_name}", + "principal": "apigateway.amazonaws.com", + "qualifier": "\${aws_lambda_alias.test-api-lambda_endpoint-lambda_alias_A14F4FF8.name}", + "source_arn": "\${aws_api_gateway_rest_api.api-gateway-rest.execution_arn}/\${aws_api_gateway_stage.api-gateway-stage.stage_name}/\${aws_api_gateway_method.test-api-lambda_endpoint-method_B2F73D1A.http_method}\${aws_api_gateway_resource.test-api-lambda_endpoint_02B05EB1.path}" + } + }, + "aws_route53_record": { + "test-api-lambda_api-gateway-certificate_certificate_record_BED7C8FC": { + "depends_on": [ + "aws_acm_certificate.test-api-lambda_api-gateway-certificate_8D77B940" + ], + "name": "\${tolist(aws_acm_certificate.test-api-lambda_api-gateway-certificate_8D77B940.domain_validation_options).0.resource_record_name}", + "records": [ + "\${tolist(aws_acm_certificate.test-api-lambda_api-gateway-certificate_8D77B940.domain_validation_options).0.resource_record_value}" + ], + "ttl": 60, + "type": "\${tolist(aws_acm_certificate.test-api-lambda_api-gateway-certificate_8D77B940.domain_validation_options).0.resource_record_type}", + "zone_id": "\${aws_route53_zone.test-api-lambda_base-dns_subhosted_zone_B29688D6.zone_id}" + }, + "test-api-lambda_apigateway-route53-domain-record_A8180A05": { + "alias": { + "evaluate_target_health": true, + "name": "\${aws_api_gateway_domain_name.test-api-lambda_api-gateway-domain-name_6DBA5B05.cloudfront_domain_name}", + "zone_id": "\${aws_api_gateway_domain_name.test-api-lambda_api-gateway-domain-name_6DBA5B05.cloudfront_zone_id}" + }, + "depends_on": [ + "aws_acm_certificate_validation.test-api-lambda_api-gateway-certificate_certificate_validation_6C09E593" + ], + "name": "\${aws_api_gateway_domain_name.test-api-lambda_api-gateway-domain-name_6DBA5B05.domain_name}", + "type": "A", + "zone_id": "\${aws_route53_zone.test-api-lambda_base-dns_subhosted_zone_B29688D6.zone_id}" + }, + "test-api-lambda_base-dns_subhosted_zone_ns_B7A2A51A": { + "name": "exampleapi.getpocket.dev", + "records": "\${aws_route53_zone.test-api-lambda_base-dns_subhosted_zone_B29688D6.name_servers}", + "ttl": 86400, + "type": "NS", + "zone_id": "\${data.aws_route53_zone.test-api-lambda_base-dns_main_hosted_zone_5F97294A.zone_id}" + } + }, + "aws_route53_zone": { + "test-api-lambda_base-dns_subhosted_zone_B29688D6": { + "name": "exampleapi.getpocket.dev" + } + }, + "aws_s3_bucket": { + "test-api-lambda_endpoint-lambda_code-bucket_8153D419": { + "bucket": "pocket-lambda-endpoint", + "force_destroy": true + } + }, + "aws_s3_bucket_acl": { + "test-api-lambda_endpoint-lambda_code-bucket-acl_769F2274": { + "acl": "private", + "bucket": "\${aws_s3_bucket.test-api-lambda_endpoint-lambda_code-bucket_8153D419.id}", + "depends_on": [ + "aws_s3_bucket_ownership_controls.test-api-lambda_endpoint-lambda_code-bucket-ownership-controls_C5FD8140" + ] + } + }, + "aws_s3_bucket_ownership_controls": { + "test-api-lambda_endpoint-lambda_code-bucket-ownership-controls_C5FD8140": { + "bucket": "\${aws_s3_bucket.test-api-lambda_endpoint-lambda_code-bucket_8153D419.id}", + "rule": { + "object_ownership": "BucketOwnerPreferred" + } + } + }, + "aws_s3_bucket_public_access_block": { + "test-api-lambda_endpoint-lambda_code-bucket-public-access-block_704E5627": { + "block_public_acls": true, + "block_public_policy": true, + "bucket": "\${aws_s3_bucket.test-api-lambda_endpoint-lambda_code-bucket_8153D419.id}" + } + } + } +}" +`; diff --git a/packages/terraform-modules/src/pocket/__snapshots__/PocketCloudwatchSynthetics.spec.ts.snap b/packages/terraform-modules/src/pocket/__snapshots__/PocketCloudwatchSynthetics.spec.ts.snap new file mode 100644 index 00000000..7c18c3d9 --- /dev/null +++ b/packages/terraform-modules/src/pocket/__snapshots__/PocketCloudwatchSynthetics.spec.ts.snap @@ -0,0 +1,469 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Pocket Cloudwatch Synthetics adds optional Alarms & Alarm Actions to Synthetic Checks 1`] = ` +"{ + "data": { + "aws_iam_policy_document": { + "test-synthetics_test-synthetics_synthetic_check_access_C1AC681E": { + "statement": [ + { + "actions": [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents" + ], + "effect": "Allow", + "resources": [ + "*" + ] + }, + { + "actions": [ + "s3:PutObject", + "s3:GetObject" + ], + "resources": [ + "\${aws_s3_bucket.test-synthetics_test-synthetics_synthetic_check_artifacts_6B281C96.arn}/*" + ] + }, + { + "actions": [ + "s3:GetObject" + ], + "resources": [ + "arn:aws:s3:::pocket-syntheticchecks-dev/*" + ] + }, + { + "actions": [ + "s3:GetBucketLocation" + ], + "resources": [ + "\${aws_s3_bucket.test-synthetics_test-synthetics_synthetic_check_artifacts_6B281C96.arn}" + ] + }, + { + "actions": [ + "s3:ListAllMyBuckets" + ], + "resources": [ + "*" + ] + }, + { + "actions": [ + "cloudwatch:PutMetricData" + ], + "condition": [ + { + "test": "StringEquals", + "values": [ + "CloudWatchSynthetics" + ], + "variable": "cloudwatch:namespace" + } + ], + "resources": [ + "*" + ] + }, + { + "actions": [ + "ec2:AttachNetworkInterface", + "ec2:CreateNetworkInterface", + "ec2:DeleteNetworkInterface", + "ec2:DescribeNetworkInterfaces" + ], + "resources": [ + "*" + ] + } + ], + "version": "2012-10-17" + }, + "test-synthetics_test-synthetics_synthetic_check_assume_9E375AF4": { + "statement": [ + { + "actions": [ + "sts:AssumeRole" + ], + "effect": "Allow", + "principals": [ + { + "identifiers": [ + "lambda.amazonaws.com" + ], + "type": "Service" + } + ] + } + ], + "version": "2012-10-17" + } + } + }, + "resource": { + "aws_cloudwatch_metric_alarm": { + "test-synthetics_test-synthetics_synthetic_check_alarm_query_0_161FA4E1": { + "alarm_actions": [ + "arn:aws:sns:us-east-1:123456789101:Test-Sns-Topic" + ], + "alarm_description": "Alert when \${aws_synthetics_canary.test-synthetics_test-synthetics_synthetic_check_query_0_ECB1B9B3.name} canary success percentage has decreased below 66% in the last 15 minutes", + "alarm_name": "\${aws_synthetics_canary.test-synthetics_test-synthetics_synthetic_check_query_0_ECB1B9B3.name}", + "comparison_operator": "LessThanThreshold", + "dimensions": { + "CanaryName": "\${aws_synthetics_canary.test-synthetics_test-synthetics_synthetic_check_query_0_ECB1B9B3.name}" + }, + "evaluation_periods": 3, + "insufficient_data_actions": [ + ], + "metric_name": "SuccessPercent", + "namespace": "CloudWatchSynthetics", + "ok_actions": [ + "arn:aws:sns:us-east-1:123456789101:Test-Sns-Topic" + ], + "period": 300, + "statistic": "Average", + "threshold": 66, + "treat_missing_data": "breaching" + }, + "test-synthetics_test-synthetics_synthetic_check_alarm_uptime_0_47F383EB": { + "alarm_actions": [ + "arn:aws:sns:us-east-1:123456789101:Test-Sns-Topic" + ], + "alarm_description": "Alert when \${aws_synthetics_canary.test-synthetics_test-synthetics_synthetic_check_uptime_0_2398A479.name} canary success percentage has decreased below 66% in the last 15 minutes", + "alarm_name": "\${aws_synthetics_canary.test-synthetics_test-synthetics_synthetic_check_uptime_0_2398A479.name}", + "comparison_operator": "LessThanThreshold", + "dimensions": { + "CanaryName": "\${aws_synthetics_canary.test-synthetics_test-synthetics_synthetic_check_uptime_0_2398A479.name}" + }, + "evaluation_periods": 3, + "insufficient_data_actions": [ + ], + "metric_name": "SuccessPercent", + "namespace": "CloudWatchSynthetics", + "ok_actions": [ + "arn:aws:sns:us-east-1:123456789101:Test-Sns-Topic" + ], + "period": 300, + "statistic": "Average", + "threshold": 66, + "treat_missing_data": "breaching" + } + }, + "aws_iam_policy": { + "test-synthetics_test-synthetics_synthetic_check_access_policy_F0C0ABD7": { + "name": "pocket-acme-dev-synthetic-check-access", + "policy": "\${data.aws_iam_policy_document.test-synthetics_test-synthetics_synthetic_check_access_C1AC681E.json}" + } + }, + "aws_iam_role": { + "test-synthetics_synthetic_check_role_B358E7A9": { + "assume_role_policy": "\${data.aws_iam_policy_document.test-synthetics_test-synthetics_synthetic_check_assume_9E375AF4.json}", + "name": "pocket-acme-dev-synthetic-check" + } + }, + "aws_iam_role_policy_attachment": { + "test-synthetics_test-synthetics_synthetic_check_access_attach_4D7F0A0A": { + "policy_arn": "\${aws_iam_policy.test-synthetics_test-synthetics_synthetic_check_access_policy_F0C0ABD7.arn}", + "role": "\${aws_iam_role.test-synthetics_synthetic_check_role_B358E7A9.id}" + } + }, + "aws_s3_bucket": { + "test-synthetics_test-synthetics_synthetic_check_artifacts_6B281C96": { + "bucket": "pocket-acme-dev-synthetic-checks" + } + }, + "aws_s3_bucket_lifecycle_configuration": { + "test-synthetics_test-synthetics_synthetic_check_artifacts_lifecycle_4D55C3CA": { + "bucket": "\${aws_s3_bucket.test-synthetics_test-synthetics_synthetic_check_artifacts_6B281C96.id}", + "rule": [ + { + "expiration": { + "days": 30 + }, + "id": "30-day-retention", + "status": "Enabled" + } + ] + } + }, + "aws_synthetics_canary": { + "test-synthetics_test-synthetics_synthetic_check_query_0_ECB1B9B3": { + "artifact_s3_location": "s3://\${aws_s3_bucket.test-synthetics_test-synthetics_synthetic_check_artifacts_6B281C96.bucket}/", + "execution_role_arn": "\${aws_iam_role.test-synthetics_synthetic_check_role_B358E7A9.arn}", + "handler": "synthetic.query", + "name": "acme-dev-query-0", + "run_config": { + "environment_variables": { + "GRAPHQL_ENDPOINT": "acme.getpocket.dev", + "GRAPHQL_JMESPATH": "errors[0].message", + "GRAPHQL_QUERY": "{\\"query\\": \\"query { someGraphQlQuery(arg1: \\\\\\"1\\\\\\", arg2: \\\\\\"1\\\\\\") {returnedAttr} }\\"}", + "GRAPHQL_RESPONSE": "Error - Not Found: A resource by that arg1 could not be found", + "GRAPHQL_USERID": "1" + }, + "timeout_in_seconds": 180 + }, + "runtime_version": "syn-nodejs-puppeteer-4.0", + "s3_bucket": "pocket-syntheticchecks-dev", + "s3_key": "aws-synthetic-dev.zip", + "schedule": { + "expression": "rate(5 minutes)" + }, + "start_canary": true + }, + "test-synthetics_test-synthetics_synthetic_check_uptime_0_2398A479": { + "artifact_s3_location": "s3://\${aws_s3_bucket.test-synthetics_test-synthetics_synthetic_check_artifacts_6B281C96.bucket}/", + "execution_role_arn": "\${aws_iam_role.test-synthetics_synthetic_check_role_B358E7A9.arn}", + "handler": "synthetic.uptime", + "name": "acme-dev-uptime-0", + "run_config": { + "environment_variables": { + "UPTIME_BODY": "ok", + "UPTIME_URL": "acme.getpocket.dev/.well-known/apollo/server-health" + }, + "timeout_in_seconds": 180 + }, + "runtime_version": "syn-nodejs-puppeteer-4.0", + "s3_bucket": "pocket-syntheticchecks-dev", + "s3_key": "aws-synthetic-dev.zip", + "schedule": { + "expression": "rate(5 minutes)" + }, + "start_canary": true + } + } + } +}" +`; + +exports[`Pocket Cloudwatch Synthetics renders desired AWS Synthetic Checks 1`] = ` +"{ + "data": { + "aws_iam_policy_document": { + "test-synthetics_test-synthetics_synthetic_check_access_C1AC681E": { + "statement": [ + { + "actions": [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents" + ], + "effect": "Allow", + "resources": [ + "*" + ] + }, + { + "actions": [ + "s3:PutObject", + "s3:GetObject" + ], + "resources": [ + "\${aws_s3_bucket.test-synthetics_test-synthetics_synthetic_check_artifacts_6B281C96.arn}/*" + ] + }, + { + "actions": [ + "s3:GetObject" + ], + "resources": [ + "arn:aws:s3:::pocket-syntheticchecks-dev/*" + ] + }, + { + "actions": [ + "s3:GetBucketLocation" + ], + "resources": [ + "\${aws_s3_bucket.test-synthetics_test-synthetics_synthetic_check_artifacts_6B281C96.arn}" + ] + }, + { + "actions": [ + "s3:ListAllMyBuckets" + ], + "resources": [ + "*" + ] + }, + { + "actions": [ + "cloudwatch:PutMetricData" + ], + "condition": [ + { + "test": "StringEquals", + "values": [ + "CloudWatchSynthetics" + ], + "variable": "cloudwatch:namespace" + } + ], + "resources": [ + "*" + ] + }, + { + "actions": [ + "ec2:AttachNetworkInterface", + "ec2:CreateNetworkInterface", + "ec2:DeleteNetworkInterface", + "ec2:DescribeNetworkInterfaces" + ], + "resources": [ + "*" + ] + } + ], + "version": "2012-10-17" + }, + "test-synthetics_test-synthetics_synthetic_check_assume_9E375AF4": { + "statement": [ + { + "actions": [ + "sts:AssumeRole" + ], + "effect": "Allow", + "principals": [ + { + "identifiers": [ + "lambda.amazonaws.com" + ], + "type": "Service" + } + ] + } + ], + "version": "2012-10-17" + } + } + }, + "resource": { + "aws_cloudwatch_metric_alarm": { + "test-synthetics_test-synthetics_synthetic_check_alarm_query_0_161FA4E1": { + "alarm_actions": [ + ], + "alarm_description": "Alert when \${aws_synthetics_canary.test-synthetics_test-synthetics_synthetic_check_query_0_ECB1B9B3.name} canary success percentage has decreased below 66% in the last 15 minutes", + "alarm_name": "\${aws_synthetics_canary.test-synthetics_test-synthetics_synthetic_check_query_0_ECB1B9B3.name}", + "comparison_operator": "LessThanThreshold", + "dimensions": { + "CanaryName": "\${aws_synthetics_canary.test-synthetics_test-synthetics_synthetic_check_query_0_ECB1B9B3.name}" + }, + "evaluation_periods": 3, + "insufficient_data_actions": [ + ], + "metric_name": "SuccessPercent", + "namespace": "CloudWatchSynthetics", + "ok_actions": [ + ], + "period": 300, + "statistic": "Average", + "threshold": 66, + "treat_missing_data": "breaching" + }, + "test-synthetics_test-synthetics_synthetic_check_alarm_uptime_0_47F383EB": { + "alarm_actions": null, + "alarm_description": "Alert when \${aws_synthetics_canary.test-synthetics_test-synthetics_synthetic_check_uptime_0_2398A479.name} canary success percentage has decreased below 66% in the last 15 minutes", + "alarm_name": "\${aws_synthetics_canary.test-synthetics_test-synthetics_synthetic_check_uptime_0_2398A479.name}", + "comparison_operator": "LessThanThreshold", + "dimensions": { + "CanaryName": "\${aws_synthetics_canary.test-synthetics_test-synthetics_synthetic_check_uptime_0_2398A479.name}" + }, + "evaluation_periods": 3, + "insufficient_data_actions": [ + ], + "metric_name": "SuccessPercent", + "namespace": "CloudWatchSynthetics", + "ok_actions": null, + "period": 300, + "statistic": "Average", + "threshold": 66, + "treat_missing_data": "breaching" + } + }, + "aws_iam_policy": { + "test-synthetics_test-synthetics_synthetic_check_access_policy_F0C0ABD7": { + "name": "pocket-acme-dev-synthetic-check-access", + "policy": "\${data.aws_iam_policy_document.test-synthetics_test-synthetics_synthetic_check_access_C1AC681E.json}" + } + }, + "aws_iam_role": { + "test-synthetics_synthetic_check_role_B358E7A9": { + "assume_role_policy": "\${data.aws_iam_policy_document.test-synthetics_test-synthetics_synthetic_check_assume_9E375AF4.json}", + "name": "pocket-acme-dev-synthetic-check" + } + }, + "aws_iam_role_policy_attachment": { + "test-synthetics_test-synthetics_synthetic_check_access_attach_4D7F0A0A": { + "policy_arn": "\${aws_iam_policy.test-synthetics_test-synthetics_synthetic_check_access_policy_F0C0ABD7.arn}", + "role": "\${aws_iam_role.test-synthetics_synthetic_check_role_B358E7A9.id}" + } + }, + "aws_s3_bucket": { + "test-synthetics_test-synthetics_synthetic_check_artifacts_6B281C96": { + "bucket": "pocket-acme-dev-synthetic-checks" + } + }, + "aws_s3_bucket_lifecycle_configuration": { + "test-synthetics_test-synthetics_synthetic_check_artifacts_lifecycle_4D55C3CA": { + "bucket": "\${aws_s3_bucket.test-synthetics_test-synthetics_synthetic_check_artifacts_6B281C96.id}", + "rule": [ + { + "expiration": { + "days": 30 + }, + "id": "30-day-retention", + "status": "Enabled" + } + ] + } + }, + "aws_synthetics_canary": { + "test-synthetics_test-synthetics_synthetic_check_query_0_ECB1B9B3": { + "artifact_s3_location": "s3://\${aws_s3_bucket.test-synthetics_test-synthetics_synthetic_check_artifacts_6B281C96.bucket}/", + "execution_role_arn": "\${aws_iam_role.test-synthetics_synthetic_check_role_B358E7A9.arn}", + "handler": "synthetic.query", + "name": "acme-dev-query-0", + "run_config": { + "environment_variables": { + "GRAPHQL_ENDPOINT": "acme.getpocket.dev", + "GRAPHQL_JMESPATH": "errors[0].message", + "GRAPHQL_QUERY": "{\\"query\\": \\"query { someGraphQlQuery(arg1: \\\\\\"1\\\\\\", arg2: \\\\\\"1\\\\\\") {returnedAttr} }\\"}", + "GRAPHQL_RESPONSE": "Error - Not Found: A resource by that arg1 could not be found", + "GRAPHQL_USERID": "1" + }, + "timeout_in_seconds": 180 + }, + "runtime_version": "syn-nodejs-puppeteer-4.0", + "s3_bucket": "pocket-syntheticchecks-dev", + "s3_key": "aws-synthetic-dev.zip", + "schedule": { + "expression": "rate(5 minutes)" + }, + "start_canary": true + }, + "test-synthetics_test-synthetics_synthetic_check_uptime_0_2398A479": { + "artifact_s3_location": "s3://\${aws_s3_bucket.test-synthetics_test-synthetics_synthetic_check_artifacts_6B281C96.bucket}/", + "execution_role_arn": "\${aws_iam_role.test-synthetics_synthetic_check_role_B358E7A9.arn}", + "handler": "synthetic.uptime", + "name": "acme-dev-uptime-0", + "run_config": { + "environment_variables": { + "UPTIME_BODY": "ok", + "UPTIME_URL": "acme.getpocket.dev/.well-known/apollo/server-health" + }, + "timeout_in_seconds": 180 + }, + "runtime_version": "syn-nodejs-puppeteer-4.0", + "s3_bucket": "pocket-syntheticchecks-dev", + "s3_key": "aws-synthetic-dev.zip", + "schedule": { + "expression": "rate(5 minutes)" + }, + "start_canary": true + } + } + } +}" +`; diff --git a/packages/terraform-modules/src/pocket/__snapshots__/PocketECSApplication.spec.ts.snap b/packages/terraform-modules/src/pocket/__snapshots__/PocketECSApplication.spec.ts.snap new file mode 100644 index 00000000..42278a48 --- /dev/null +++ b/packages/terraform-modules/src/pocket/__snapshots__/PocketECSApplication.spec.ts.snap @@ -0,0 +1,2106 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`PocketECSApplication renders an Pocket App with logs and dashboard in a specified region 1`] = ` +"{ + "data": { + "aws_caller_identity": { + "testPocketApp_pocket_vpc_current_identity_06D87057": { + } + }, + "aws_iam_policy_document": { + "testPocketApp_ecs_service_ecs-iam_ecs-task-assume_ABB81680": { + "statement": [ + { + "actions": [ + "sts:AssumeRole" + ], + "effect": "Allow", + "principals": [ + { + "identifiers": [ + "ecs-tasks.amazonaws.com" + ], + "type": "Service" + } + ] + } + ], + "version": "2012-10-17" + } + }, + "aws_kms_alias": { + "testPocketApp_pocket_vpc_secrets_manager_key_9FD2BC93": { + "name": "alias/aws/secretsmanager" + } + }, + "aws_region": { + "testPocketApp_pocket_vpc_current_region_8ED435E7": { + } + }, + "aws_security_groups": { + "testPocketApp_pocket_vpc_default_security_groups_40B4FC48": { + "filter": [ + { + "name": "group-name", + "values": [ + "default" + ] + }, + { + "name": "vpc-id", + "values": [ + "\${data.aws_vpc.testPocketApp_pocket_vpc_C4E157E3.id}" + ] + } + ] + }, + "testPocketApp_pocket_vpc_internal_security_groups_12F7F666": { + "filter": [ + { + "name": "group-name", + "values": [ + "pocket-vpc-internal" + ] + }, + { + "name": "vpc-id", + "values": [ + "\${data.aws_vpc.testPocketApp_pocket_vpc_C4E157E3.id}" + ] + } + ] + } + }, + "aws_ssm_parameter": { + "testPocketApp_pocket_vpc_private_subnets_9D449563": { + "name": "/Shared/PrivateSubnets" + }, + "testPocketApp_pocket_vpc_public_subnets_B0B3A6AB": { + "name": "/Shared/PublicSubnets" + }, + "testPocketApp_pocket_vpc_vpc_ssm_param_235525D4": { + "name": "/Shared/Vpc" + } + }, + "aws_subnets": { + "testPocketApp_pocket_vpc_private_subnet_ids_EB1E3A65": { + "filter": [ + { + "name": "subnet-id", + "values": "\${split(\\",\\", data.aws_ssm_parameter.testPocketApp_pocket_vpc_private_subnets_9D449563.value)}" + }, + { + "name": "vpc-id", + "values": [ + "\${data.aws_vpc.testPocketApp_pocket_vpc_C4E157E3.id}" + ] + } + ] + }, + "testPocketApp_pocket_vpc_public_subnet_ids_01F0B902": { + "filter": [ + { + "name": "subnet-id", + "values": "\${split(\\",\\", data.aws_ssm_parameter.testPocketApp_pocket_vpc_public_subnets_B0B3A6AB.value)}" + }, + { + "name": "vpc-id", + "values": [ + "\${data.aws_vpc.testPocketApp_pocket_vpc_C4E157E3.id}" + ] + } + ] + } + }, + "aws_vpc": { + "testPocketApp_pocket_vpc_C4E157E3": { + "filter": [ + { + "name": "vpc-id", + "values": [ + "\${data.aws_ssm_parameter.testPocketApp_pocket_vpc_vpc_ssm_param_235525D4.value}" + ] + } + ] + } + } + }, + "resource": { + "aws_appautoscaling_policy": { + "testPocketApp_autoscaling_scale_in_policy_A163A235": { + "depends_on": [ + "aws_appautoscaling_target.testPocketApp_autoscaling_autoscaling_target_505E4EA1" + ], + "name": "testapp-ScaleInPolicy", + "policy_type": "StepScaling", + "resource_id": "\${aws_appautoscaling_target.testPocketApp_autoscaling_autoscaling_target_505E4EA1.resource_id}", + "scalable_dimension": "\${aws_appautoscaling_target.testPocketApp_autoscaling_autoscaling_target_505E4EA1.scalable_dimension}", + "service_namespace": "\${aws_appautoscaling_target.testPocketApp_autoscaling_autoscaling_target_505E4EA1.service_namespace}", + "step_scaling_policy_configuration": { + "adjustment_type": "ChangeInCapacity", + "cooldown": 60, + "metric_aggregation_type": "Average", + "step_adjustment": [ + { + "metric_interval_upper_bound": "0", + "scaling_adjustment": -1 + } + ] + }, + "target_tracking_scaling_policy_configuration": null + }, + "testPocketApp_autoscaling_scale_out_policy_075BFAC4": { + "depends_on": [ + "aws_appautoscaling_target.testPocketApp_autoscaling_autoscaling_target_505E4EA1" + ], + "name": "testapp-ScaleOutPolicy", + "policy_type": "StepScaling", + "resource_id": "\${aws_appautoscaling_target.testPocketApp_autoscaling_autoscaling_target_505E4EA1.resource_id}", + "scalable_dimension": "\${aws_appautoscaling_target.testPocketApp_autoscaling_autoscaling_target_505E4EA1.scalable_dimension}", + "service_namespace": "\${aws_appautoscaling_target.testPocketApp_autoscaling_autoscaling_target_505E4EA1.service_namespace}", + "step_scaling_policy_configuration": { + "adjustment_type": "ChangeInCapacity", + "cooldown": 60, + "metric_aggregation_type": "Average", + "step_adjustment": [ + { + "metric_interval_lower_bound": "0", + "scaling_adjustment": 2 + } + ] + }, + "target_tracking_scaling_policy_configuration": null + } + }, + "aws_appautoscaling_target": { + "testPocketApp_autoscaling_autoscaling_target_505E4EA1": { + "max_capacity": 2, + "min_capacity": 1, + "resource_id": "service/\${aws_ecs_cluster.testPocketApp_ecs_cluster_C3960066.name}/\${aws_ecs_service.testPocketApp_ecs_service_ecs-service_182DEA4C.name}", + "scalable_dimension": "ecs:service:DesiredCount", + "service_namespace": "ecs" + } + }, + "aws_cloudwatch_dashboard": { + "testPocketApp_cloudwatch-dashboard_26E9F70C": { + "dashboard_body": "{\\"widgets\\":[{\\"type\\":\\"metric\\",\\"x\\":0,\\"y\\":6,\\"width\\":12,\\"height\\":6,\\"properties\\":{\\"metrics\\":[[\\"ECS/ContainerInsights\\",\\"RunningTaskCount\\",\\"ServiceName\\",\\"\${aws_ecs_service.testPocketApp_ecs_service_ecs-service_182DEA4C.name}\\",\\"ClusterName\\",\\"\${aws_ecs_cluster.testPocketApp_ecs_cluster_C3960066.name}\\",{\\"yAxis\\":\\"right\\",\\"color\\":\\"#c49c94\\"}],[\\"AWS/ECS\\",\\"CPUUtilization\\",\\".\\",\\".\\",\\".\\",\\".\\",{\\"color\\":\\"#f7b6d2\\"}],[\\".\\",\\"MemoryUtilization\\",\\".\\",\\".\\",\\".\\",\\".\\",{\\"color\\":\\"#c7c7c7\\"}]],\\"view\\":\\"timeSeries\\",\\"stacked\\":false,\\"region\\":\\"central region\\",\\"stat\\":\\"Average\\",\\"period\\":60,\\"annotations\\":{\\"horizontal\\":[{\\"color\\":\\"#e377c2\\",\\"label\\":\\"CPU scale out\\",\\"value\\":45},{\\"color\\":\\"#c5b0d5\\",\\"label\\":\\"CPU scale in\\",\\"value\\":30}]},\\"title\\":\\"Service Load\\"}}]}", + "dashboard_name": "testapp", + "lifecycle": { + "ignore_changes": [ + "dashboard_body" + ] + } + } + }, + "aws_cloudwatch_metric_alarm": { + "testPocketApp_autoscaling_scale_in_alarm_69B5F00D": { + "alarm_actions": [ + "\${aws_appautoscaling_policy.testPocketApp_autoscaling_scale_in_policy_A163A235.arn}" + ], + "alarm_description": "Alarm to reduce capacity if container CPU is low", + "alarm_name": "testapp Service Low CPU", + "comparison_operator": "LessThanThreshold", + "dimensions": { + "ClusterName": "\${aws_ecs_cluster.testPocketApp_ecs_cluster_C3960066.name}", + "ServiceName": "\${aws_ecs_service.testPocketApp_ecs_service_ecs-service_182DEA4C.name}" + }, + "evaluation_periods": 2, + "metric_name": "CPUUtilization", + "namespace": "AWS/ECS", + "period": 60, + "statistic": "Average", + "threshold": 30, + "treat_missing_data": "notBreaching" + }, + "testPocketApp_autoscaling_scale_out_alarm_4313FBE9": { + "alarm_actions": [ + "\${aws_appautoscaling_policy.testPocketApp_autoscaling_scale_out_policy_075BFAC4.arn}" + ], + "alarm_description": "Alarm to add capacity if container CPU is high", + "alarm_name": "testapp Service High CPU", + "comparison_operator": "GreaterThanThreshold", + "dimensions": { + "ClusterName": "\${aws_ecs_cluster.testPocketApp_ecs_cluster_C3960066.name}", + "ServiceName": "\${aws_ecs_service.testPocketApp_ecs_service_ecs-service_182DEA4C.name}" + }, + "evaluation_periods": 2, + "metric_name": "CPUUtilization", + "namespace": "AWS/ECS", + "period": 60, + "statistic": "Average", + "threshold": 45, + "treat_missing_data": "notBreaching" + } + }, + "aws_ecs_cluster": { + "testPocketApp_ecs_cluster_C3960066": { + "name": "testapp", + "setting": [ + { + "name": "containerInsights", + "value": "enabled" + } + ] + } + }, + "aws_ecs_service": { + "testPocketApp_ecs_service_ecs-service_182DEA4C": { + "cluster": "\${aws_ecs_cluster.testPocketApp_ecs_cluster_C3960066.arn}", + "depends_on": [ + ], + "deployment_controller": { + "type": "ECS" + }, + "deployment_maximum_percent": 200, + "deployment_minimum_healthy_percent": 100, + "desired_count": 2, + "launch_type": "FARGATE", + "lifecycle": { + "ignore_changes": [ + "desired_count", + "load_balancer" + ] + }, + "load_balancer": [ + ], + "name": "testapp", + "network_configuration": { + "security_groups": [ + "\${aws_security_group.testPocketApp_ecs_service_ecs_security_group_9C016FA8.id}" + ], + "subnets": "\${data.aws_subnets.testPocketApp_pocket_vpc_private_subnet_ids_EB1E3A65.ids}" + }, + "propagate_tags": "SERVICE", + "task_definition": "\${aws_ecs_task_definition.testPocketApp_ecs_service_ecs-task_A7E74E45.arn}" + } + }, + "aws_ecs_task_definition": { + "testPocketApp_ecs_service_ecs-task_A7E74E45": { + "container_definitions": "[]", + "cpu": "512", + "depends_on": [ + ], + "execution_role_arn": "\${aws_iam_role.testPocketApp_ecs_service_ecs-iam_ecs-execution-role_EB461A0C.arn}", + "family": "testapp", + "memory": "2048", + "network_mode": "awsvpc", + "requires_compatibilities": [ + "FARGATE" + ], + "task_role_arn": "\${aws_iam_role.testPocketApp_ecs_service_ecs-iam_ecs-task-role_7DB93963.arn}", + "volume": [ + ] + } + }, + "aws_iam_role": { + "testPocketApp_ecs_service_ecs-iam_ecs-execution-role_EB461A0C": { + "assume_role_policy": "\${data.aws_iam_policy_document.testPocketApp_ecs_service_ecs-iam_ecs-task-assume_ABB81680.json}", + "name": "testapp-TaskExecutionRole" + }, + "testPocketApp_ecs_service_ecs-iam_ecs-task-role_7DB93963": { + "assume_role_policy": "\${data.aws_iam_policy_document.testPocketApp_ecs_service_ecs-iam_ecs-task-assume_ABB81680.json}", + "name": "testapp-TaskRole" + } + }, + "aws_security_group": { + "testPocketApp_ecs_service_ecs_security_group_9C016FA8": { + "description": "Internal ECS Security Group (Managed by Terraform)", + "egress": [ + { + "cidr_blocks": [ + "0.0.0.0/0" + ], + "description": "required", + "from_port": 0, + "ipv6_cidr_blocks": [ + ], + "prefix_list_ids": [ + ], + "protocol": "-1", + "security_groups": [ + ], + "self": null, + "to_port": 0 + } + ], + "ingress": [ + ], + "lifecycle": { + "create_before_destroy": true + }, + "name_prefix": "testapp-ECSSecurityGroup", + "vpc_id": "\${data.aws_vpc.testPocketApp_pocket_vpc_C4E157E3.id}" + } + } + } +}" +`; + +exports[`PocketECSApplication renders an application with autoscaling group and tags 1`] = ` +"{ + "data": { + "aws_caller_identity": { + "testPocketApp_pocket_vpc_current_identity_06D87057": { + } + }, + "aws_iam_policy_document": { + "testPocketApp_ecs_service_ecs-iam_ecs-task-assume_ABB81680": { + "statement": [ + { + "actions": [ + "sts:AssumeRole" + ], + "effect": "Allow", + "principals": [ + { + "identifiers": [ + "ecs-tasks.amazonaws.com" + ], + "type": "Service" + } + ] + } + ], + "version": "2012-10-17" + } + }, + "aws_kms_alias": { + "testPocketApp_pocket_vpc_secrets_manager_key_9FD2BC93": { + "name": "alias/aws/secretsmanager" + } + }, + "aws_region": { + "testPocketApp_pocket_vpc_current_region_8ED435E7": { + } + }, + "aws_security_groups": { + "testPocketApp_pocket_vpc_default_security_groups_40B4FC48": { + "filter": [ + { + "name": "group-name", + "values": [ + "default" + ] + }, + { + "name": "vpc-id", + "values": [ + "\${data.aws_vpc.testPocketApp_pocket_vpc_C4E157E3.id}" + ] + } + ] + }, + "testPocketApp_pocket_vpc_internal_security_groups_12F7F666": { + "filter": [ + { + "name": "group-name", + "values": [ + "pocket-vpc-internal" + ] + }, + { + "name": "vpc-id", + "values": [ + "\${data.aws_vpc.testPocketApp_pocket_vpc_C4E157E3.id}" + ] + } + ] + } + }, + "aws_ssm_parameter": { + "testPocketApp_pocket_vpc_private_subnets_9D449563": { + "name": "/Shared/PrivateSubnets" + }, + "testPocketApp_pocket_vpc_public_subnets_B0B3A6AB": { + "name": "/Shared/PublicSubnets" + }, + "testPocketApp_pocket_vpc_vpc_ssm_param_235525D4": { + "name": "/Shared/Vpc" + } + }, + "aws_subnets": { + "testPocketApp_pocket_vpc_private_subnet_ids_EB1E3A65": { + "filter": [ + { + "name": "subnet-id", + "values": "\${split(\\",\\", data.aws_ssm_parameter.testPocketApp_pocket_vpc_private_subnets_9D449563.value)}" + }, + { + "name": "vpc-id", + "values": [ + "\${data.aws_vpc.testPocketApp_pocket_vpc_C4E157E3.id}" + ] + } + ] + }, + "testPocketApp_pocket_vpc_public_subnet_ids_01F0B902": { + "filter": [ + { + "name": "subnet-id", + "values": "\${split(\\",\\", data.aws_ssm_parameter.testPocketApp_pocket_vpc_public_subnets_B0B3A6AB.value)}" + }, + { + "name": "vpc-id", + "values": [ + "\${data.aws_vpc.testPocketApp_pocket_vpc_C4E157E3.id}" + ] + } + ] + } + }, + "aws_vpc": { + "testPocketApp_pocket_vpc_C4E157E3": { + "filter": [ + { + "name": "vpc-id", + "values": [ + "\${data.aws_ssm_parameter.testPocketApp_pocket_vpc_vpc_ssm_param_235525D4.value}" + ] + } + ] + } + } + }, + "resource": { + "aws_appautoscaling_policy": { + "testPocketApp_autoscaling_scale_in_policy_A163A235": { + "depends_on": [ + "aws_appautoscaling_target.testPocketApp_autoscaling_autoscaling_target_505E4EA1" + ], + "name": "testapp-ScaleInPolicy", + "policy_type": "StepScaling", + "resource_id": "\${aws_appautoscaling_target.testPocketApp_autoscaling_autoscaling_target_505E4EA1.resource_id}", + "scalable_dimension": "\${aws_appautoscaling_target.testPocketApp_autoscaling_autoscaling_target_505E4EA1.scalable_dimension}", + "service_namespace": "\${aws_appautoscaling_target.testPocketApp_autoscaling_autoscaling_target_505E4EA1.service_namespace}", + "step_scaling_policy_configuration": { + "adjustment_type": "ChangeInCapacity", + "cooldown": 60, + "metric_aggregation_type": "Average", + "step_adjustment": [ + { + "metric_interval_upper_bound": "0", + "scaling_adjustment": -1 + } + ] + }, + "target_tracking_scaling_policy_configuration": null + }, + "testPocketApp_autoscaling_scale_out_policy_075BFAC4": { + "depends_on": [ + "aws_appautoscaling_target.testPocketApp_autoscaling_autoscaling_target_505E4EA1" + ], + "name": "testapp-ScaleOutPolicy", + "policy_type": "StepScaling", + "resource_id": "\${aws_appautoscaling_target.testPocketApp_autoscaling_autoscaling_target_505E4EA1.resource_id}", + "scalable_dimension": "\${aws_appautoscaling_target.testPocketApp_autoscaling_autoscaling_target_505E4EA1.scalable_dimension}", + "service_namespace": "\${aws_appautoscaling_target.testPocketApp_autoscaling_autoscaling_target_505E4EA1.service_namespace}", + "step_scaling_policy_configuration": { + "adjustment_type": "ChangeInCapacity", + "cooldown": 60, + "metric_aggregation_type": "Average", + "step_adjustment": [ + { + "metric_interval_lower_bound": "0", + "scaling_adjustment": 2 + } + ] + }, + "target_tracking_scaling_policy_configuration": null + } + }, + "aws_appautoscaling_target": { + "testPocketApp_autoscaling_autoscaling_target_505E4EA1": { + "max_capacity": 2, + "min_capacity": 1, + "resource_id": "service/\${aws_ecs_cluster.testPocketApp_ecs_cluster_C3960066.name}/\${aws_ecs_service.testPocketApp_ecs_service_ecs-service_182DEA4C.name}", + "scalable_dimension": "ecs:service:DesiredCount", + "service_namespace": "ecs" + } + }, + "aws_cloudwatch_dashboard": { + "testPocketApp_cloudwatch-dashboard_26E9F70C": { + "dashboard_body": "{\\"widgets\\":[{\\"type\\":\\"metric\\",\\"x\\":0,\\"y\\":6,\\"width\\":12,\\"height\\":6,\\"properties\\":{\\"metrics\\":[[\\"ECS/ContainerInsights\\",\\"RunningTaskCount\\",\\"ServiceName\\",\\"\${aws_ecs_service.testPocketApp_ecs_service_ecs-service_182DEA4C.name}\\",\\"ClusterName\\",\\"\${aws_ecs_cluster.testPocketApp_ecs_cluster_C3960066.name}\\",{\\"yAxis\\":\\"right\\",\\"color\\":\\"#c49c94\\"}],[\\"AWS/ECS\\",\\"CPUUtilization\\",\\".\\",\\".\\",\\".\\",\\".\\",{\\"color\\":\\"#f7b6d2\\"}],[\\".\\",\\"MemoryUtilization\\",\\".\\",\\".\\",\\".\\",\\".\\",{\\"color\\":\\"#c7c7c7\\"}]],\\"view\\":\\"timeSeries\\",\\"stacked\\":false,\\"region\\":\\"us-east-1\\",\\"stat\\":\\"Average\\",\\"period\\":60,\\"annotations\\":{\\"horizontal\\":[{\\"color\\":\\"#e377c2\\",\\"label\\":\\"CPU scale out\\",\\"value\\":45},{\\"color\\":\\"#c5b0d5\\",\\"label\\":\\"CPU scale in\\",\\"value\\":30}]},\\"title\\":\\"Service Load\\"}}]}", + "dashboard_name": "testapp", + "lifecycle": { + "ignore_changes": [ + "dashboard_body" + ] + } + } + }, + "aws_cloudwatch_metric_alarm": { + "testPocketApp_autoscaling_scale_in_alarm_69B5F00D": { + "alarm_actions": [ + "\${aws_appautoscaling_policy.testPocketApp_autoscaling_scale_in_policy_A163A235.arn}" + ], + "alarm_description": "Alarm to reduce capacity if container CPU is low", + "alarm_name": "testapp Service Low CPU", + "comparison_operator": "LessThanThreshold", + "dimensions": { + "ClusterName": "\${aws_ecs_cluster.testPocketApp_ecs_cluster_C3960066.name}", + "ServiceName": "\${aws_ecs_service.testPocketApp_ecs_service_ecs-service_182DEA4C.name}" + }, + "evaluation_periods": 2, + "metric_name": "CPUUtilization", + "namespace": "AWS/ECS", + "period": 60, + "statistic": "Average", + "tags": { + "hobby": "bowling", + "name": "thedude" + }, + "threshold": 30, + "treat_missing_data": "notBreaching" + }, + "testPocketApp_autoscaling_scale_out_alarm_4313FBE9": { + "alarm_actions": [ + "\${aws_appautoscaling_policy.testPocketApp_autoscaling_scale_out_policy_075BFAC4.arn}" + ], + "alarm_description": "Alarm to add capacity if container CPU is high", + "alarm_name": "testapp Service High CPU", + "comparison_operator": "GreaterThanThreshold", + "dimensions": { + "ClusterName": "\${aws_ecs_cluster.testPocketApp_ecs_cluster_C3960066.name}", + "ServiceName": "\${aws_ecs_service.testPocketApp_ecs_service_ecs-service_182DEA4C.name}" + }, + "evaluation_periods": 2, + "metric_name": "CPUUtilization", + "namespace": "AWS/ECS", + "period": 60, + "statistic": "Average", + "tags": { + "hobby": "bowling", + "name": "thedude" + }, + "threshold": 45, + "treat_missing_data": "notBreaching" + } + }, + "aws_ecs_cluster": { + "testPocketApp_ecs_cluster_C3960066": { + "name": "testapp", + "setting": [ + { + "name": "containerInsights", + "value": "enabled" + } + ], + "tags": { + "hobby": "bowling", + "name": "thedude" + } + } + }, + "aws_ecs_service": { + "testPocketApp_ecs_service_ecs-service_182DEA4C": { + "cluster": "\${aws_ecs_cluster.testPocketApp_ecs_cluster_C3960066.arn}", + "depends_on": [ + ], + "deployment_controller": { + "type": "ECS" + }, + "deployment_maximum_percent": 200, + "deployment_minimum_healthy_percent": 100, + "desired_count": 2, + "launch_type": "FARGATE", + "lifecycle": { + "ignore_changes": [ + "desired_count", + "load_balancer" + ] + }, + "load_balancer": [ + ], + "name": "testapp", + "network_configuration": { + "security_groups": [ + "\${aws_security_group.testPocketApp_ecs_service_ecs_security_group_9C016FA8.id}" + ], + "subnets": "\${data.aws_subnets.testPocketApp_pocket_vpc_private_subnet_ids_EB1E3A65.ids}" + }, + "propagate_tags": "SERVICE", + "tags": { + "hobby": "bowling", + "name": "thedude" + }, + "task_definition": "\${aws_ecs_task_definition.testPocketApp_ecs_service_ecs-task_A7E74E45.arn}" + } + }, + "aws_ecs_task_definition": { + "testPocketApp_ecs_service_ecs-task_A7E74E45": { + "container_definitions": "[]", + "cpu": "512", + "depends_on": [ + ], + "execution_role_arn": "\${aws_iam_role.testPocketApp_ecs_service_ecs-iam_ecs-execution-role_EB461A0C.arn}", + "family": "testapp", + "memory": "2048", + "network_mode": "awsvpc", + "requires_compatibilities": [ + "FARGATE" + ], + "tags": { + "hobby": "bowling", + "name": "thedude" + }, + "task_role_arn": "\${aws_iam_role.testPocketApp_ecs_service_ecs-iam_ecs-task-role_7DB93963.arn}", + "volume": [ + ] + } + }, + "aws_iam_role": { + "testPocketApp_ecs_service_ecs-iam_ecs-execution-role_EB461A0C": { + "assume_role_policy": "\${data.aws_iam_policy_document.testPocketApp_ecs_service_ecs-iam_ecs-task-assume_ABB81680.json}", + "name": "testapp-TaskExecutionRole", + "tags": { + "hobby": "bowling", + "name": "thedude" + } + }, + "testPocketApp_ecs_service_ecs-iam_ecs-task-role_7DB93963": { + "assume_role_policy": "\${data.aws_iam_policy_document.testPocketApp_ecs_service_ecs-iam_ecs-task-assume_ABB81680.json}", + "name": "testapp-TaskRole", + "tags": { + "hobby": "bowling", + "name": "thedude" + } + } + }, + "aws_security_group": { + "testPocketApp_ecs_service_ecs_security_group_9C016FA8": { + "description": "Internal ECS Security Group (Managed by Terraform)", + "egress": [ + { + "cidr_blocks": [ + "0.0.0.0/0" + ], + "description": "required", + "from_port": 0, + "ipv6_cidr_blocks": [ + ], + "prefix_list_ids": [ + ], + "protocol": "-1", + "security_groups": [ + ], + "self": null, + "to_port": 0 + } + ], + "ingress": [ + ], + "lifecycle": { + "create_before_destroy": true + }, + "name_prefix": "testapp-ECSSecurityGroup", + "tags": { + "hobby": "bowling", + "name": "thedude" + }, + "vpc_id": "\${data.aws_vpc.testPocketApp_pocket_vpc_C4E157E3.id}" + } + } + } +}" +`; + +exports[`PocketECSApplication renders an application with custom task sizes 1`] = ` +"{ + "data": { + "aws_caller_identity": { + "testPocketApp_pocket_vpc_current_identity_06D87057": { + } + }, + "aws_iam_policy_document": { + "testPocketApp_ecs_service_ecs-iam_ecs-task-assume_ABB81680": { + "statement": [ + { + "actions": [ + "sts:AssumeRole" + ], + "effect": "Allow", + "principals": [ + { + "identifiers": [ + "ecs-tasks.amazonaws.com" + ], + "type": "Service" + } + ] + } + ], + "version": "2012-10-17" + } + }, + "aws_kms_alias": { + "testPocketApp_pocket_vpc_secrets_manager_key_9FD2BC93": { + "name": "alias/aws/secretsmanager" + } + }, + "aws_region": { + "testPocketApp_pocket_vpc_current_region_8ED435E7": { + } + }, + "aws_security_groups": { + "testPocketApp_pocket_vpc_default_security_groups_40B4FC48": { + "filter": [ + { + "name": "group-name", + "values": [ + "default" + ] + }, + { + "name": "vpc-id", + "values": [ + "\${data.aws_vpc.testPocketApp_pocket_vpc_C4E157E3.id}" + ] + } + ] + }, + "testPocketApp_pocket_vpc_internal_security_groups_12F7F666": { + "filter": [ + { + "name": "group-name", + "values": [ + "pocket-vpc-internal" + ] + }, + { + "name": "vpc-id", + "values": [ + "\${data.aws_vpc.testPocketApp_pocket_vpc_C4E157E3.id}" + ] + } + ] + } + }, + "aws_ssm_parameter": { + "testPocketApp_pocket_vpc_private_subnets_9D449563": { + "name": "/Shared/PrivateSubnets" + }, + "testPocketApp_pocket_vpc_public_subnets_B0B3A6AB": { + "name": "/Shared/PublicSubnets" + }, + "testPocketApp_pocket_vpc_vpc_ssm_param_235525D4": { + "name": "/Shared/Vpc" + } + }, + "aws_subnets": { + "testPocketApp_pocket_vpc_private_subnet_ids_EB1E3A65": { + "filter": [ + { + "name": "subnet-id", + "values": "\${split(\\",\\", data.aws_ssm_parameter.testPocketApp_pocket_vpc_private_subnets_9D449563.value)}" + }, + { + "name": "vpc-id", + "values": [ + "\${data.aws_vpc.testPocketApp_pocket_vpc_C4E157E3.id}" + ] + } + ] + }, + "testPocketApp_pocket_vpc_public_subnet_ids_01F0B902": { + "filter": [ + { + "name": "subnet-id", + "values": "\${split(\\",\\", data.aws_ssm_parameter.testPocketApp_pocket_vpc_public_subnets_B0B3A6AB.value)}" + }, + { + "name": "vpc-id", + "values": [ + "\${data.aws_vpc.testPocketApp_pocket_vpc_C4E157E3.id}" + ] + } + ] + } + }, + "aws_vpc": { + "testPocketApp_pocket_vpc_C4E157E3": { + "filter": [ + { + "name": "vpc-id", + "values": [ + "\${data.aws_ssm_parameter.testPocketApp_pocket_vpc_vpc_ssm_param_235525D4.value}" + ] + } + ] + } + } + }, + "resource": { + "aws_appautoscaling_policy": { + "testPocketApp_autoscaling_scale_in_policy_A163A235": { + "depends_on": [ + "aws_appautoscaling_target.testPocketApp_autoscaling_autoscaling_target_505E4EA1" + ], + "name": "testapp-ScaleInPolicy", + "policy_type": "StepScaling", + "resource_id": "\${aws_appautoscaling_target.testPocketApp_autoscaling_autoscaling_target_505E4EA1.resource_id}", + "scalable_dimension": "\${aws_appautoscaling_target.testPocketApp_autoscaling_autoscaling_target_505E4EA1.scalable_dimension}", + "service_namespace": "\${aws_appautoscaling_target.testPocketApp_autoscaling_autoscaling_target_505E4EA1.service_namespace}", + "step_scaling_policy_configuration": { + "adjustment_type": "ChangeInCapacity", + "cooldown": 60, + "metric_aggregation_type": "Average", + "step_adjustment": [ + { + "metric_interval_upper_bound": "0", + "scaling_adjustment": -1 + } + ] + }, + "target_tracking_scaling_policy_configuration": null + }, + "testPocketApp_autoscaling_scale_out_policy_075BFAC4": { + "depends_on": [ + "aws_appautoscaling_target.testPocketApp_autoscaling_autoscaling_target_505E4EA1" + ], + "name": "testapp-ScaleOutPolicy", + "policy_type": "StepScaling", + "resource_id": "\${aws_appautoscaling_target.testPocketApp_autoscaling_autoscaling_target_505E4EA1.resource_id}", + "scalable_dimension": "\${aws_appautoscaling_target.testPocketApp_autoscaling_autoscaling_target_505E4EA1.scalable_dimension}", + "service_namespace": "\${aws_appautoscaling_target.testPocketApp_autoscaling_autoscaling_target_505E4EA1.service_namespace}", + "step_scaling_policy_configuration": { + "adjustment_type": "ChangeInCapacity", + "cooldown": 60, + "metric_aggregation_type": "Average", + "step_adjustment": [ + { + "metric_interval_lower_bound": "0", + "scaling_adjustment": 2 + } + ] + }, + "target_tracking_scaling_policy_configuration": null + } + }, + "aws_appautoscaling_target": { + "testPocketApp_autoscaling_autoscaling_target_505E4EA1": { + "max_capacity": 2, + "min_capacity": 1, + "resource_id": "service/\${aws_ecs_cluster.testPocketApp_ecs_cluster_C3960066.name}/\${aws_ecs_service.testPocketApp_ecs_service_ecs-service_182DEA4C.name}", + "scalable_dimension": "ecs:service:DesiredCount", + "service_namespace": "ecs" + } + }, + "aws_cloudwatch_dashboard": { + "testPocketApp_cloudwatch-dashboard_26E9F70C": { + "dashboard_body": "{\\"widgets\\":[{\\"type\\":\\"metric\\",\\"x\\":0,\\"y\\":6,\\"width\\":12,\\"height\\":6,\\"properties\\":{\\"metrics\\":[[\\"ECS/ContainerInsights\\",\\"RunningTaskCount\\",\\"ServiceName\\",\\"\${aws_ecs_service.testPocketApp_ecs_service_ecs-service_182DEA4C.name}\\",\\"ClusterName\\",\\"\${aws_ecs_cluster.testPocketApp_ecs_cluster_C3960066.name}\\",{\\"yAxis\\":\\"right\\",\\"color\\":\\"#c49c94\\"}],[\\"AWS/ECS\\",\\"CPUUtilization\\",\\".\\",\\".\\",\\".\\",\\".\\",{\\"color\\":\\"#f7b6d2\\"}],[\\".\\",\\"MemoryUtilization\\",\\".\\",\\".\\",\\".\\",\\".\\",{\\"color\\":\\"#c7c7c7\\"}]],\\"view\\":\\"timeSeries\\",\\"stacked\\":false,\\"region\\":\\"us-east-1\\",\\"stat\\":\\"Average\\",\\"period\\":60,\\"annotations\\":{\\"horizontal\\":[{\\"color\\":\\"#e377c2\\",\\"label\\":\\"CPU scale out\\",\\"value\\":45},{\\"color\\":\\"#c5b0d5\\",\\"label\\":\\"CPU scale in\\",\\"value\\":30}]},\\"title\\":\\"Service Load\\"}}]}", + "dashboard_name": "testapp", + "lifecycle": { + "ignore_changes": [ + "dashboard_body" + ] + } + } + }, + "aws_cloudwatch_metric_alarm": { + "testPocketApp_autoscaling_scale_in_alarm_69B5F00D": { + "alarm_actions": [ + "\${aws_appautoscaling_policy.testPocketApp_autoscaling_scale_in_policy_A163A235.arn}" + ], + "alarm_description": "Alarm to reduce capacity if container CPU is low", + "alarm_name": "testapp Service Low CPU", + "comparison_operator": "LessThanThreshold", + "dimensions": { + "ClusterName": "\${aws_ecs_cluster.testPocketApp_ecs_cluster_C3960066.name}", + "ServiceName": "\${aws_ecs_service.testPocketApp_ecs_service_ecs-service_182DEA4C.name}" + }, + "evaluation_periods": 2, + "metric_name": "CPUUtilization", + "namespace": "AWS/ECS", + "period": 60, + "statistic": "Average", + "threshold": 30, + "treat_missing_data": "notBreaching" + }, + "testPocketApp_autoscaling_scale_out_alarm_4313FBE9": { + "alarm_actions": [ + "\${aws_appautoscaling_policy.testPocketApp_autoscaling_scale_out_policy_075BFAC4.arn}" + ], + "alarm_description": "Alarm to add capacity if container CPU is high", + "alarm_name": "testapp Service High CPU", + "comparison_operator": "GreaterThanThreshold", + "dimensions": { + "ClusterName": "\${aws_ecs_cluster.testPocketApp_ecs_cluster_C3960066.name}", + "ServiceName": "\${aws_ecs_service.testPocketApp_ecs_service_ecs-service_182DEA4C.name}" + }, + "evaluation_periods": 2, + "metric_name": "CPUUtilization", + "namespace": "AWS/ECS", + "period": 60, + "statistic": "Average", + "threshold": 45, + "treat_missing_data": "notBreaching" + } + }, + "aws_ecs_cluster": { + "testPocketApp_ecs_cluster_C3960066": { + "name": "testapp", + "setting": [ + { + "name": "containerInsights", + "value": "enabled" + } + ] + } + }, + "aws_ecs_service": { + "testPocketApp_ecs_service_ecs-service_182DEA4C": { + "cluster": "\${aws_ecs_cluster.testPocketApp_ecs_cluster_C3960066.arn}", + "depends_on": [ + ], + "deployment_controller": { + "type": "ECS" + }, + "deployment_maximum_percent": 200, + "deployment_minimum_healthy_percent": 100, + "desired_count": 2, + "launch_type": "FARGATE", + "lifecycle": { + "ignore_changes": [ + "desired_count", + "load_balancer" + ] + }, + "load_balancer": [ + ], + "name": "testapp", + "network_configuration": { + "security_groups": [ + "\${aws_security_group.testPocketApp_ecs_service_ecs_security_group_9C016FA8.id}" + ], + "subnets": "\${data.aws_subnets.testPocketApp_pocket_vpc_private_subnet_ids_EB1E3A65.ids}" + }, + "propagate_tags": "SERVICE", + "task_definition": "\${aws_ecs_task_definition.testPocketApp_ecs_service_ecs-task_A7E74E45.arn}" + } + }, + "aws_ecs_task_definition": { + "testPocketApp_ecs_service_ecs-task_A7E74E45": { + "container_definitions": "[]", + "cpu": "8675", + "depends_on": [ + ], + "execution_role_arn": "\${aws_iam_role.testPocketApp_ecs_service_ecs-iam_ecs-execution-role_EB461A0C.arn}", + "family": "testapp", + "memory": "309", + "network_mode": "awsvpc", + "requires_compatibilities": [ + "FARGATE" + ], + "task_role_arn": "\${aws_iam_role.testPocketApp_ecs_service_ecs-iam_ecs-task-role_7DB93963.arn}", + "volume": [ + ] + } + }, + "aws_iam_role": { + "testPocketApp_ecs_service_ecs-iam_ecs-execution-role_EB461A0C": { + "assume_role_policy": "\${data.aws_iam_policy_document.testPocketApp_ecs_service_ecs-iam_ecs-task-assume_ABB81680.json}", + "name": "testapp-TaskExecutionRole" + }, + "testPocketApp_ecs_service_ecs-iam_ecs-task-role_7DB93963": { + "assume_role_policy": "\${data.aws_iam_policy_document.testPocketApp_ecs_service_ecs-iam_ecs-task-assume_ABB81680.json}", + "name": "testapp-TaskRole" + } + }, + "aws_security_group": { + "testPocketApp_ecs_service_ecs_security_group_9C016FA8": { + "description": "Internal ECS Security Group (Managed by Terraform)", + "egress": [ + { + "cidr_blocks": [ + "0.0.0.0/0" + ], + "description": "required", + "from_port": 0, + "ipv6_cidr_blocks": [ + ], + "prefix_list_ids": [ + ], + "protocol": "-1", + "security_groups": [ + ], + "self": null, + "to_port": 0 + } + ], + "ingress": [ + ], + "lifecycle": { + "create_before_destroy": true + }, + "name_prefix": "testapp-ECSSecurityGroup", + "vpc_id": "\${data.aws_vpc.testPocketApp_pocket_vpc_C4E157E3.id}" + } + } + } +}" +`; + +exports[`PocketECSApplication renders an application with default autoscaling group and tags 1`] = ` +"{ + "data": { + "aws_caller_identity": { + "testPocketApp_pocket_vpc_current_identity_06D87057": { + } + }, + "aws_iam_policy_document": { + "testPocketApp_ecs_service_ecs-iam_ecs-task-assume_ABB81680": { + "statement": [ + { + "actions": [ + "sts:AssumeRole" + ], + "effect": "Allow", + "principals": [ + { + "identifiers": [ + "ecs-tasks.amazonaws.com" + ], + "type": "Service" + } + ] + } + ], + "version": "2012-10-17" + } + }, + "aws_kms_alias": { + "testPocketApp_pocket_vpc_secrets_manager_key_9FD2BC93": { + "name": "alias/aws/secretsmanager" + } + }, + "aws_region": { + "testPocketApp_pocket_vpc_current_region_8ED435E7": { + } + }, + "aws_security_groups": { + "testPocketApp_pocket_vpc_default_security_groups_40B4FC48": { + "filter": [ + { + "name": "group-name", + "values": [ + "default" + ] + }, + { + "name": "vpc-id", + "values": [ + "\${data.aws_vpc.testPocketApp_pocket_vpc_C4E157E3.id}" + ] + } + ] + }, + "testPocketApp_pocket_vpc_internal_security_groups_12F7F666": { + "filter": [ + { + "name": "group-name", + "values": [ + "pocket-vpc-internal" + ] + }, + { + "name": "vpc-id", + "values": [ + "\${data.aws_vpc.testPocketApp_pocket_vpc_C4E157E3.id}" + ] + } + ] + } + }, + "aws_ssm_parameter": { + "testPocketApp_pocket_vpc_private_subnets_9D449563": { + "name": "/Shared/PrivateSubnets" + }, + "testPocketApp_pocket_vpc_public_subnets_B0B3A6AB": { + "name": "/Shared/PublicSubnets" + }, + "testPocketApp_pocket_vpc_vpc_ssm_param_235525D4": { + "name": "/Shared/Vpc" + } + }, + "aws_subnets": { + "testPocketApp_pocket_vpc_private_subnet_ids_EB1E3A65": { + "filter": [ + { + "name": "subnet-id", + "values": "\${split(\\",\\", data.aws_ssm_parameter.testPocketApp_pocket_vpc_private_subnets_9D449563.value)}" + }, + { + "name": "vpc-id", + "values": [ + "\${data.aws_vpc.testPocketApp_pocket_vpc_C4E157E3.id}" + ] + } + ] + }, + "testPocketApp_pocket_vpc_public_subnet_ids_01F0B902": { + "filter": [ + { + "name": "subnet-id", + "values": "\${split(\\",\\", data.aws_ssm_parameter.testPocketApp_pocket_vpc_public_subnets_B0B3A6AB.value)}" + }, + { + "name": "vpc-id", + "values": [ + "\${data.aws_vpc.testPocketApp_pocket_vpc_C4E157E3.id}" + ] + } + ] + } + }, + "aws_vpc": { + "testPocketApp_pocket_vpc_C4E157E3": { + "filter": [ + { + "name": "vpc-id", + "values": [ + "\${data.aws_ssm_parameter.testPocketApp_pocket_vpc_vpc_ssm_param_235525D4.value}" + ] + } + ] + } + } + }, + "resource": { + "aws_appautoscaling_policy": { + "testPocketApp_autoscaling_scale_in_policy_A163A235": { + "depends_on": [ + "aws_appautoscaling_target.testPocketApp_autoscaling_autoscaling_target_505E4EA1" + ], + "name": "testapp-ScaleInPolicy", + "policy_type": "StepScaling", + "resource_id": "\${aws_appautoscaling_target.testPocketApp_autoscaling_autoscaling_target_505E4EA1.resource_id}", + "scalable_dimension": "\${aws_appautoscaling_target.testPocketApp_autoscaling_autoscaling_target_505E4EA1.scalable_dimension}", + "service_namespace": "\${aws_appautoscaling_target.testPocketApp_autoscaling_autoscaling_target_505E4EA1.service_namespace}", + "step_scaling_policy_configuration": { + "adjustment_type": "ChangeInCapacity", + "cooldown": 60, + "metric_aggregation_type": "Average", + "step_adjustment": [ + { + "metric_interval_upper_bound": "0", + "scaling_adjustment": -1 + } + ] + }, + "target_tracking_scaling_policy_configuration": null + }, + "testPocketApp_autoscaling_scale_out_policy_075BFAC4": { + "depends_on": [ + "aws_appautoscaling_target.testPocketApp_autoscaling_autoscaling_target_505E4EA1" + ], + "name": "testapp-ScaleOutPolicy", + "policy_type": "StepScaling", + "resource_id": "\${aws_appautoscaling_target.testPocketApp_autoscaling_autoscaling_target_505E4EA1.resource_id}", + "scalable_dimension": "\${aws_appautoscaling_target.testPocketApp_autoscaling_autoscaling_target_505E4EA1.scalable_dimension}", + "service_namespace": "\${aws_appautoscaling_target.testPocketApp_autoscaling_autoscaling_target_505E4EA1.service_namespace}", + "step_scaling_policy_configuration": { + "adjustment_type": "ChangeInCapacity", + "cooldown": 60, + "metric_aggregation_type": "Average", + "step_adjustment": [ + { + "metric_interval_lower_bound": "0", + "scaling_adjustment": 2 + } + ] + }, + "target_tracking_scaling_policy_configuration": null + } + }, + "aws_appautoscaling_target": { + "testPocketApp_autoscaling_autoscaling_target_505E4EA1": { + "max_capacity": 2, + "min_capacity": 1, + "resource_id": "service/\${aws_ecs_cluster.testPocketApp_ecs_cluster_C3960066.name}/\${aws_ecs_service.testPocketApp_ecs_service_ecs-service_182DEA4C.name}", + "scalable_dimension": "ecs:service:DesiredCount", + "service_namespace": "ecs" + } + }, + "aws_cloudwatch_dashboard": { + "testPocketApp_cloudwatch-dashboard_26E9F70C": { + "dashboard_body": "{\\"widgets\\":[{\\"type\\":\\"metric\\",\\"x\\":0,\\"y\\":6,\\"width\\":12,\\"height\\":6,\\"properties\\":{\\"metrics\\":[[\\"ECS/ContainerInsights\\",\\"RunningTaskCount\\",\\"ServiceName\\",\\"\${aws_ecs_service.testPocketApp_ecs_service_ecs-service_182DEA4C.name}\\",\\"ClusterName\\",\\"\${aws_ecs_cluster.testPocketApp_ecs_cluster_C3960066.name}\\",{\\"yAxis\\":\\"right\\",\\"color\\":\\"#c49c94\\"}],[\\"AWS/ECS\\",\\"CPUUtilization\\",\\".\\",\\".\\",\\".\\",\\".\\",{\\"color\\":\\"#f7b6d2\\"}],[\\".\\",\\"MemoryUtilization\\",\\".\\",\\".\\",\\".\\",\\".\\",{\\"color\\":\\"#c7c7c7\\"}]],\\"view\\":\\"timeSeries\\",\\"stacked\\":false,\\"region\\":\\"us-east-1\\",\\"stat\\":\\"Average\\",\\"period\\":60,\\"annotations\\":{\\"horizontal\\":[{\\"color\\":\\"#e377c2\\",\\"label\\":\\"CPU scale out\\",\\"value\\":45},{\\"color\\":\\"#c5b0d5\\",\\"label\\":\\"CPU scale in\\",\\"value\\":30}]},\\"title\\":\\"Service Load\\"}}]}", + "dashboard_name": "testapp", + "lifecycle": { + "ignore_changes": [ + "dashboard_body" + ] + } + } + }, + "aws_cloudwatch_metric_alarm": { + "testPocketApp_autoscaling_scale_in_alarm_69B5F00D": { + "alarm_actions": [ + "\${aws_appautoscaling_policy.testPocketApp_autoscaling_scale_in_policy_A163A235.arn}" + ], + "alarm_description": "Alarm to reduce capacity if container CPU is low", + "alarm_name": "testapp Service Low CPU", + "comparison_operator": "LessThanThreshold", + "dimensions": { + "ClusterName": "\${aws_ecs_cluster.testPocketApp_ecs_cluster_C3960066.name}", + "ServiceName": "\${aws_ecs_service.testPocketApp_ecs_service_ecs-service_182DEA4C.name}" + }, + "evaluation_periods": 2, + "metric_name": "CPUUtilization", + "namespace": "AWS/ECS", + "period": 60, + "statistic": "Average", + "tags": { + "hobby": "bowling", + "name": "thedude" + }, + "threshold": 30, + "treat_missing_data": "notBreaching" + }, + "testPocketApp_autoscaling_scale_out_alarm_4313FBE9": { + "alarm_actions": [ + "\${aws_appautoscaling_policy.testPocketApp_autoscaling_scale_out_policy_075BFAC4.arn}" + ], + "alarm_description": "Alarm to add capacity if container CPU is high", + "alarm_name": "testapp Service High CPU", + "comparison_operator": "GreaterThanThreshold", + "dimensions": { + "ClusterName": "\${aws_ecs_cluster.testPocketApp_ecs_cluster_C3960066.name}", + "ServiceName": "\${aws_ecs_service.testPocketApp_ecs_service_ecs-service_182DEA4C.name}" + }, + "evaluation_periods": 2, + "metric_name": "CPUUtilization", + "namespace": "AWS/ECS", + "period": 60, + "statistic": "Average", + "tags": { + "hobby": "bowling", + "name": "thedude" + }, + "threshold": 45, + "treat_missing_data": "notBreaching" + } + }, + "aws_ecs_cluster": { + "testPocketApp_ecs_cluster_C3960066": { + "name": "testapp", + "setting": [ + { + "name": "containerInsights", + "value": "enabled" + } + ], + "tags": { + "hobby": "bowling", + "name": "thedude" + } + } + }, + "aws_ecs_service": { + "testPocketApp_ecs_service_ecs-service_182DEA4C": { + "cluster": "\${aws_ecs_cluster.testPocketApp_ecs_cluster_C3960066.arn}", + "depends_on": [ + ], + "deployment_controller": { + "type": "ECS" + }, + "deployment_maximum_percent": 200, + "deployment_minimum_healthy_percent": 100, + "desired_count": 2, + "launch_type": "FARGATE", + "lifecycle": { + "ignore_changes": [ + "desired_count", + "load_balancer" + ] + }, + "load_balancer": [ + ], + "name": "testapp", + "network_configuration": { + "security_groups": [ + "\${aws_security_group.testPocketApp_ecs_service_ecs_security_group_9C016FA8.id}" + ], + "subnets": "\${data.aws_subnets.testPocketApp_pocket_vpc_private_subnet_ids_EB1E3A65.ids}" + }, + "propagate_tags": "SERVICE", + "tags": { + "hobby": "bowling", + "name": "thedude" + }, + "task_definition": "\${aws_ecs_task_definition.testPocketApp_ecs_service_ecs-task_A7E74E45.arn}" + } + }, + "aws_ecs_task_definition": { + "testPocketApp_ecs_service_ecs-task_A7E74E45": { + "container_definitions": "[]", + "cpu": "512", + "depends_on": [ + ], + "execution_role_arn": "\${aws_iam_role.testPocketApp_ecs_service_ecs-iam_ecs-execution-role_EB461A0C.arn}", + "family": "testapp", + "memory": "2048", + "network_mode": "awsvpc", + "requires_compatibilities": [ + "FARGATE" + ], + "tags": { + "hobby": "bowling", + "name": "thedude" + }, + "task_role_arn": "\${aws_iam_role.testPocketApp_ecs_service_ecs-iam_ecs-task-role_7DB93963.arn}", + "volume": [ + ] + } + }, + "aws_iam_role": { + "testPocketApp_ecs_service_ecs-iam_ecs-execution-role_EB461A0C": { + "assume_role_policy": "\${data.aws_iam_policy_document.testPocketApp_ecs_service_ecs-iam_ecs-task-assume_ABB81680.json}", + "name": "testapp-TaskExecutionRole", + "tags": { + "hobby": "bowling", + "name": "thedude" + } + }, + "testPocketApp_ecs_service_ecs-iam_ecs-task-role_7DB93963": { + "assume_role_policy": "\${data.aws_iam_policy_document.testPocketApp_ecs_service_ecs-iam_ecs-task-assume_ABB81680.json}", + "name": "testapp-TaskRole", + "tags": { + "hobby": "bowling", + "name": "thedude" + } + } + }, + "aws_security_group": { + "testPocketApp_ecs_service_ecs_security_group_9C016FA8": { + "description": "Internal ECS Security Group (Managed by Terraform)", + "egress": [ + { + "cidr_blocks": [ + "0.0.0.0/0" + ], + "description": "required", + "from_port": 0, + "ipv6_cidr_blocks": [ + ], + "prefix_list_ids": [ + ], + "protocol": "-1", + "security_groups": [ + ], + "self": null, + "to_port": 0 + } + ], + "ingress": [ + ], + "lifecycle": { + "create_before_destroy": true + }, + "name_prefix": "testapp-ECSSecurityGroup", + "tags": { + "hobby": "bowling", + "name": "thedude" + }, + "vpc_id": "\${data.aws_vpc.testPocketApp_pocket_vpc_C4E157E3.id}" + } + } + } +}" +`; + +exports[`PocketECSApplication renders an application with minimal config 1`] = ` +"{ + "data": { + "aws_caller_identity": { + "testPocketApp_pocket_vpc_current_identity_06D87057": { + } + }, + "aws_iam_policy_document": { + "testPocketApp_ecs_service_ecs-iam_ecs-task-assume_ABB81680": { + "statement": [ + { + "actions": [ + "sts:AssumeRole" + ], + "effect": "Allow", + "principals": [ + { + "identifiers": [ + "ecs-tasks.amazonaws.com" + ], + "type": "Service" + } + ] + } + ], + "version": "2012-10-17" + } + }, + "aws_kms_alias": { + "testPocketApp_pocket_vpc_secrets_manager_key_9FD2BC93": { + "name": "alias/aws/secretsmanager" + } + }, + "aws_region": { + "testPocketApp_pocket_vpc_current_region_8ED435E7": { + } + }, + "aws_security_groups": { + "testPocketApp_pocket_vpc_default_security_groups_40B4FC48": { + "filter": [ + { + "name": "group-name", + "values": [ + "default" + ] + }, + { + "name": "vpc-id", + "values": [ + "\${data.aws_vpc.testPocketApp_pocket_vpc_C4E157E3.id}" + ] + } + ] + }, + "testPocketApp_pocket_vpc_internal_security_groups_12F7F666": { + "filter": [ + { + "name": "group-name", + "values": [ + "pocket-vpc-internal" + ] + }, + { + "name": "vpc-id", + "values": [ + "\${data.aws_vpc.testPocketApp_pocket_vpc_C4E157E3.id}" + ] + } + ] + } + }, + "aws_ssm_parameter": { + "testPocketApp_pocket_vpc_private_subnets_9D449563": { + "name": "/Shared/PrivateSubnets" + }, + "testPocketApp_pocket_vpc_public_subnets_B0B3A6AB": { + "name": "/Shared/PublicSubnets" + }, + "testPocketApp_pocket_vpc_vpc_ssm_param_235525D4": { + "name": "/Shared/Vpc" + } + }, + "aws_subnets": { + "testPocketApp_pocket_vpc_private_subnet_ids_EB1E3A65": { + "filter": [ + { + "name": "subnet-id", + "values": "\${split(\\",\\", data.aws_ssm_parameter.testPocketApp_pocket_vpc_private_subnets_9D449563.value)}" + }, + { + "name": "vpc-id", + "values": [ + "\${data.aws_vpc.testPocketApp_pocket_vpc_C4E157E3.id}" + ] + } + ] + }, + "testPocketApp_pocket_vpc_public_subnet_ids_01F0B902": { + "filter": [ + { + "name": "subnet-id", + "values": "\${split(\\",\\", data.aws_ssm_parameter.testPocketApp_pocket_vpc_public_subnets_B0B3A6AB.value)}" + }, + { + "name": "vpc-id", + "values": [ + "\${data.aws_vpc.testPocketApp_pocket_vpc_C4E157E3.id}" + ] + } + ] + } + }, + "aws_vpc": { + "testPocketApp_pocket_vpc_C4E157E3": { + "filter": [ + { + "name": "vpc-id", + "values": [ + "\${data.aws_ssm_parameter.testPocketApp_pocket_vpc_vpc_ssm_param_235525D4.value}" + ] + } + ] + } + } + }, + "resource": { + "aws_appautoscaling_policy": { + "testPocketApp_autoscaling_scale_in_policy_A163A235": { + "depends_on": [ + "aws_appautoscaling_target.testPocketApp_autoscaling_autoscaling_target_505E4EA1" + ], + "name": "testapp-ScaleInPolicy", + "policy_type": "StepScaling", + "resource_id": "\${aws_appautoscaling_target.testPocketApp_autoscaling_autoscaling_target_505E4EA1.resource_id}", + "scalable_dimension": "\${aws_appautoscaling_target.testPocketApp_autoscaling_autoscaling_target_505E4EA1.scalable_dimension}", + "service_namespace": "\${aws_appautoscaling_target.testPocketApp_autoscaling_autoscaling_target_505E4EA1.service_namespace}", + "step_scaling_policy_configuration": { + "adjustment_type": "ChangeInCapacity", + "cooldown": 60, + "metric_aggregation_type": "Average", + "step_adjustment": [ + { + "metric_interval_upper_bound": "0", + "scaling_adjustment": -1 + } + ] + }, + "target_tracking_scaling_policy_configuration": null + }, + "testPocketApp_autoscaling_scale_out_policy_075BFAC4": { + "depends_on": [ + "aws_appautoscaling_target.testPocketApp_autoscaling_autoscaling_target_505E4EA1" + ], + "name": "testapp-ScaleOutPolicy", + "policy_type": "StepScaling", + "resource_id": "\${aws_appautoscaling_target.testPocketApp_autoscaling_autoscaling_target_505E4EA1.resource_id}", + "scalable_dimension": "\${aws_appautoscaling_target.testPocketApp_autoscaling_autoscaling_target_505E4EA1.scalable_dimension}", + "service_namespace": "\${aws_appautoscaling_target.testPocketApp_autoscaling_autoscaling_target_505E4EA1.service_namespace}", + "step_scaling_policy_configuration": { + "adjustment_type": "ChangeInCapacity", + "cooldown": 60, + "metric_aggregation_type": "Average", + "step_adjustment": [ + { + "metric_interval_lower_bound": "0", + "scaling_adjustment": 2 + } + ] + }, + "target_tracking_scaling_policy_configuration": null + } + }, + "aws_appautoscaling_target": { + "testPocketApp_autoscaling_autoscaling_target_505E4EA1": { + "max_capacity": 2, + "min_capacity": 1, + "resource_id": "service/\${aws_ecs_cluster.testPocketApp_ecs_cluster_C3960066.name}/\${aws_ecs_service.testPocketApp_ecs_service_ecs-service_182DEA4C.name}", + "scalable_dimension": "ecs:service:DesiredCount", + "service_namespace": "ecs" + } + }, + "aws_cloudwatch_dashboard": { + "testPocketApp_cloudwatch-dashboard_26E9F70C": { + "dashboard_body": "{\\"widgets\\":[{\\"type\\":\\"metric\\",\\"x\\":0,\\"y\\":6,\\"width\\":12,\\"height\\":6,\\"properties\\":{\\"metrics\\":[[\\"ECS/ContainerInsights\\",\\"RunningTaskCount\\",\\"ServiceName\\",\\"\${aws_ecs_service.testPocketApp_ecs_service_ecs-service_182DEA4C.name}\\",\\"ClusterName\\",\\"\${aws_ecs_cluster.testPocketApp_ecs_cluster_C3960066.name}\\",{\\"yAxis\\":\\"right\\",\\"color\\":\\"#c49c94\\"}],[\\"AWS/ECS\\",\\"CPUUtilization\\",\\".\\",\\".\\",\\".\\",\\".\\",{\\"color\\":\\"#f7b6d2\\"}],[\\".\\",\\"MemoryUtilization\\",\\".\\",\\".\\",\\".\\",\\".\\",{\\"color\\":\\"#c7c7c7\\"}]],\\"view\\":\\"timeSeries\\",\\"stacked\\":false,\\"region\\":\\"us-east-1\\",\\"stat\\":\\"Average\\",\\"period\\":60,\\"annotations\\":{\\"horizontal\\":[{\\"color\\":\\"#e377c2\\",\\"label\\":\\"CPU scale out\\",\\"value\\":45},{\\"color\\":\\"#c5b0d5\\",\\"label\\":\\"CPU scale in\\",\\"value\\":30}]},\\"title\\":\\"Service Load\\"}}]}", + "dashboard_name": "testapp", + "lifecycle": { + "ignore_changes": [ + "dashboard_body" + ] + } + } + }, + "aws_cloudwatch_metric_alarm": { + "testPocketApp_autoscaling_scale_in_alarm_69B5F00D": { + "alarm_actions": [ + "\${aws_appautoscaling_policy.testPocketApp_autoscaling_scale_in_policy_A163A235.arn}" + ], + "alarm_description": "Alarm to reduce capacity if container CPU is low", + "alarm_name": "testapp Service Low CPU", + "comparison_operator": "LessThanThreshold", + "dimensions": { + "ClusterName": "\${aws_ecs_cluster.testPocketApp_ecs_cluster_C3960066.name}", + "ServiceName": "\${aws_ecs_service.testPocketApp_ecs_service_ecs-service_182DEA4C.name}" + }, + "evaluation_periods": 2, + "metric_name": "CPUUtilization", + "namespace": "AWS/ECS", + "period": 60, + "statistic": "Average", + "threshold": 30, + "treat_missing_data": "notBreaching" + }, + "testPocketApp_autoscaling_scale_out_alarm_4313FBE9": { + "alarm_actions": [ + "\${aws_appautoscaling_policy.testPocketApp_autoscaling_scale_out_policy_075BFAC4.arn}" + ], + "alarm_description": "Alarm to add capacity if container CPU is high", + "alarm_name": "testapp Service High CPU", + "comparison_operator": "GreaterThanThreshold", + "dimensions": { + "ClusterName": "\${aws_ecs_cluster.testPocketApp_ecs_cluster_C3960066.name}", + "ServiceName": "\${aws_ecs_service.testPocketApp_ecs_service_ecs-service_182DEA4C.name}" + }, + "evaluation_periods": 2, + "metric_name": "CPUUtilization", + "namespace": "AWS/ECS", + "period": 60, + "statistic": "Average", + "threshold": 45, + "treat_missing_data": "notBreaching" + } + }, + "aws_ecs_cluster": { + "testPocketApp_ecs_cluster_C3960066": { + "name": "testapp", + "setting": [ + { + "name": "containerInsights", + "value": "enabled" + } + ] + } + }, + "aws_ecs_service": { + "testPocketApp_ecs_service_ecs-service_182DEA4C": { + "cluster": "\${aws_ecs_cluster.testPocketApp_ecs_cluster_C3960066.arn}", + "depends_on": [ + ], + "deployment_controller": { + "type": "ECS" + }, + "deployment_maximum_percent": 200, + "deployment_minimum_healthy_percent": 100, + "desired_count": 2, + "launch_type": "FARGATE", + "lifecycle": { + "ignore_changes": [ + "desired_count", + "load_balancer" + ] + }, + "load_balancer": [ + ], + "name": "testapp", + "network_configuration": { + "security_groups": [ + "\${aws_security_group.testPocketApp_ecs_service_ecs_security_group_9C016FA8.id}" + ], + "subnets": "\${data.aws_subnets.testPocketApp_pocket_vpc_private_subnet_ids_EB1E3A65.ids}" + }, + "propagate_tags": "SERVICE", + "task_definition": "\${aws_ecs_task_definition.testPocketApp_ecs_service_ecs-task_A7E74E45.arn}" + } + }, + "aws_ecs_task_definition": { + "testPocketApp_ecs_service_ecs-task_A7E74E45": { + "container_definitions": "[]", + "cpu": "512", + "depends_on": [ + ], + "execution_role_arn": "\${aws_iam_role.testPocketApp_ecs_service_ecs-iam_ecs-execution-role_EB461A0C.arn}", + "family": "testapp", + "memory": "2048", + "network_mode": "awsvpc", + "requires_compatibilities": [ + "FARGATE" + ], + "task_role_arn": "\${aws_iam_role.testPocketApp_ecs_service_ecs-iam_ecs-task-role_7DB93963.arn}", + "volume": [ + ] + } + }, + "aws_iam_role": { + "testPocketApp_ecs_service_ecs-iam_ecs-execution-role_EB461A0C": { + "assume_role_policy": "\${data.aws_iam_policy_document.testPocketApp_ecs_service_ecs-iam_ecs-task-assume_ABB81680.json}", + "name": "testapp-TaskExecutionRole" + }, + "testPocketApp_ecs_service_ecs-iam_ecs-task-role_7DB93963": { + "assume_role_policy": "\${data.aws_iam_policy_document.testPocketApp_ecs_service_ecs-iam_ecs-task-assume_ABB81680.json}", + "name": "testapp-TaskRole" + } + }, + "aws_security_group": { + "testPocketApp_ecs_service_ecs_security_group_9C016FA8": { + "description": "Internal ECS Security Group (Managed by Terraform)", + "egress": [ + { + "cidr_blocks": [ + "0.0.0.0/0" + ], + "description": "required", + "from_port": 0, + "ipv6_cidr_blocks": [ + ], + "prefix_list_ids": [ + ], + "protocol": "-1", + "security_groups": [ + ], + "self": null, + "to_port": 0 + } + ], + "ingress": [ + ], + "lifecycle": { + "create_before_destroy": true + }, + "name_prefix": "testapp-ECSSecurityGroup", + "vpc_id": "\${data.aws_vpc.testPocketApp_pocket_vpc_C4E157E3.id}" + } + } + } +}" +`; + +exports[`PocketECSApplication renders an application with modified container def protocol, cpu and memory reservation 1`] = ` +"{ + "data": { + "aws_caller_identity": { + "testPocketApp_pocket_vpc_current_identity_06D87057": { + } + }, + "aws_iam_policy_document": { + "testPocketApp_ecs_service_ecs-iam_ecs-task-assume_ABB81680": { + "statement": [ + { + "actions": [ + "sts:AssumeRole" + ], + "effect": "Allow", + "principals": [ + { + "identifiers": [ + "ecs-tasks.amazonaws.com" + ], + "type": "Service" + } + ] + } + ], + "version": "2012-10-17" + } + }, + "aws_kms_alias": { + "testPocketApp_pocket_vpc_secrets_manager_key_9FD2BC93": { + "name": "alias/aws/secretsmanager" + } + }, + "aws_region": { + "testPocketApp_pocket_vpc_current_region_8ED435E7": { + } + }, + "aws_security_groups": { + "testPocketApp_pocket_vpc_default_security_groups_40B4FC48": { + "filter": [ + { + "name": "group-name", + "values": [ + "default" + ] + }, + { + "name": "vpc-id", + "values": [ + "\${data.aws_vpc.testPocketApp_pocket_vpc_C4E157E3.id}" + ] + } + ] + }, + "testPocketApp_pocket_vpc_internal_security_groups_12F7F666": { + "filter": [ + { + "name": "group-name", + "values": [ + "pocket-vpc-internal" + ] + }, + { + "name": "vpc-id", + "values": [ + "\${data.aws_vpc.testPocketApp_pocket_vpc_C4E157E3.id}" + ] + } + ] + } + }, + "aws_ssm_parameter": { + "testPocketApp_pocket_vpc_private_subnets_9D449563": { + "name": "/Shared/PrivateSubnets" + }, + "testPocketApp_pocket_vpc_public_subnets_B0B3A6AB": { + "name": "/Shared/PublicSubnets" + }, + "testPocketApp_pocket_vpc_vpc_ssm_param_235525D4": { + "name": "/Shared/Vpc" + } + }, + "aws_subnets": { + "testPocketApp_pocket_vpc_private_subnet_ids_EB1E3A65": { + "filter": [ + { + "name": "subnet-id", + "values": "\${split(\\",\\", data.aws_ssm_parameter.testPocketApp_pocket_vpc_private_subnets_9D449563.value)}" + }, + { + "name": "vpc-id", + "values": [ + "\${data.aws_vpc.testPocketApp_pocket_vpc_C4E157E3.id}" + ] + } + ] + }, + "testPocketApp_pocket_vpc_public_subnet_ids_01F0B902": { + "filter": [ + { + "name": "subnet-id", + "values": "\${split(\\",\\", data.aws_ssm_parameter.testPocketApp_pocket_vpc_public_subnets_B0B3A6AB.value)}" + }, + { + "name": "vpc-id", + "values": [ + "\${data.aws_vpc.testPocketApp_pocket_vpc_C4E157E3.id}" + ] + } + ] + } + }, + "aws_vpc": { + "testPocketApp_pocket_vpc_C4E157E3": { + "filter": [ + { + "name": "vpc-id", + "values": [ + "\${data.aws_ssm_parameter.testPocketApp_pocket_vpc_vpc_ssm_param_235525D4.value}" + ] + } + ] + } + } + }, + "resource": { + "aws_appautoscaling_policy": { + "testPocketApp_autoscaling_scale_in_policy_A163A235": { + "depends_on": [ + "aws_appautoscaling_target.testPocketApp_autoscaling_autoscaling_target_505E4EA1" + ], + "name": "testapp-ScaleInPolicy", + "policy_type": "StepScaling", + "resource_id": "\${aws_appautoscaling_target.testPocketApp_autoscaling_autoscaling_target_505E4EA1.resource_id}", + "scalable_dimension": "\${aws_appautoscaling_target.testPocketApp_autoscaling_autoscaling_target_505E4EA1.scalable_dimension}", + "service_namespace": "\${aws_appautoscaling_target.testPocketApp_autoscaling_autoscaling_target_505E4EA1.service_namespace}", + "step_scaling_policy_configuration": { + "adjustment_type": "ChangeInCapacity", + "cooldown": 60, + "metric_aggregation_type": "Average", + "step_adjustment": [ + { + "metric_interval_upper_bound": "0", + "scaling_adjustment": -1 + } + ] + }, + "target_tracking_scaling_policy_configuration": null + }, + "testPocketApp_autoscaling_scale_out_policy_075BFAC4": { + "depends_on": [ + "aws_appautoscaling_target.testPocketApp_autoscaling_autoscaling_target_505E4EA1" + ], + "name": "testapp-ScaleOutPolicy", + "policy_type": "StepScaling", + "resource_id": "\${aws_appautoscaling_target.testPocketApp_autoscaling_autoscaling_target_505E4EA1.resource_id}", + "scalable_dimension": "\${aws_appautoscaling_target.testPocketApp_autoscaling_autoscaling_target_505E4EA1.scalable_dimension}", + "service_namespace": "\${aws_appautoscaling_target.testPocketApp_autoscaling_autoscaling_target_505E4EA1.service_namespace}", + "step_scaling_policy_configuration": { + "adjustment_type": "ChangeInCapacity", + "cooldown": 60, + "metric_aggregation_type": "Average", + "step_adjustment": [ + { + "metric_interval_lower_bound": "0", + "scaling_adjustment": 2 + } + ] + }, + "target_tracking_scaling_policy_configuration": null + } + }, + "aws_appautoscaling_target": { + "testPocketApp_autoscaling_autoscaling_target_505E4EA1": { + "max_capacity": 2, + "min_capacity": 1, + "resource_id": "service/\${aws_ecs_cluster.testPocketApp_ecs_cluster_C3960066.name}/\${aws_ecs_service.testPocketApp_ecs_service_ecs-service_182DEA4C.name}", + "scalable_dimension": "ecs:service:DesiredCount", + "service_namespace": "ecs" + } + }, + "aws_cloudwatch_dashboard": { + "testPocketApp_cloudwatch-dashboard_26E9F70C": { + "dashboard_body": "{\\"widgets\\":[{\\"type\\":\\"metric\\",\\"x\\":0,\\"y\\":6,\\"width\\":12,\\"height\\":6,\\"properties\\":{\\"metrics\\":[[\\"ECS/ContainerInsights\\",\\"RunningTaskCount\\",\\"ServiceName\\",\\"\${aws_ecs_service.testPocketApp_ecs_service_ecs-service_182DEA4C.name}\\",\\"ClusterName\\",\\"\${aws_ecs_cluster.testPocketApp_ecs_cluster_C3960066.name}\\",{\\"yAxis\\":\\"right\\",\\"color\\":\\"#c49c94\\"}],[\\"AWS/ECS\\",\\"CPUUtilization\\",\\".\\",\\".\\",\\".\\",\\".\\",{\\"color\\":\\"#f7b6d2\\"}],[\\".\\",\\"MemoryUtilization\\",\\".\\",\\".\\",\\".\\",\\".\\",{\\"color\\":\\"#c7c7c7\\"}]],\\"view\\":\\"timeSeries\\",\\"stacked\\":false,\\"region\\":\\"us-east-1\\",\\"stat\\":\\"Average\\",\\"period\\":60,\\"annotations\\":{\\"horizontal\\":[{\\"color\\":\\"#e377c2\\",\\"label\\":\\"CPU scale out\\",\\"value\\":45},{\\"color\\":\\"#c5b0d5\\",\\"label\\":\\"CPU scale in\\",\\"value\\":30}]},\\"title\\":\\"Service Load\\"}}]}", + "dashboard_name": "testapp", + "lifecycle": { + "ignore_changes": [ + "dashboard_body" + ] + } + } + }, + "aws_cloudwatch_log_group": { + "testPocketApp_ecs_service_ecs-xray-daemon_55253A74": { + "name_prefix": "/ecs/testapp/xray-daemon", + "retention_in_days": 30 + } + }, + "aws_cloudwatch_metric_alarm": { + "testPocketApp_autoscaling_scale_in_alarm_69B5F00D": { + "alarm_actions": [ + "\${aws_appautoscaling_policy.testPocketApp_autoscaling_scale_in_policy_A163A235.arn}" + ], + "alarm_description": "Alarm to reduce capacity if container CPU is low", + "alarm_name": "testapp Service Low CPU", + "comparison_operator": "LessThanThreshold", + "dimensions": { + "ClusterName": "\${aws_ecs_cluster.testPocketApp_ecs_cluster_C3960066.name}", + "ServiceName": "\${aws_ecs_service.testPocketApp_ecs_service_ecs-service_182DEA4C.name}" + }, + "evaluation_periods": 2, + "metric_name": "CPUUtilization", + "namespace": "AWS/ECS", + "period": 60, + "statistic": "Average", + "threshold": 30, + "treat_missing_data": "notBreaching" + }, + "testPocketApp_autoscaling_scale_out_alarm_4313FBE9": { + "alarm_actions": [ + "\${aws_appautoscaling_policy.testPocketApp_autoscaling_scale_out_policy_075BFAC4.arn}" + ], + "alarm_description": "Alarm to add capacity if container CPU is high", + "alarm_name": "testapp Service High CPU", + "comparison_operator": "GreaterThanThreshold", + "dimensions": { + "ClusterName": "\${aws_ecs_cluster.testPocketApp_ecs_cluster_C3960066.name}", + "ServiceName": "\${aws_ecs_service.testPocketApp_ecs_service_ecs-service_182DEA4C.name}" + }, + "evaluation_periods": 2, + "metric_name": "CPUUtilization", + "namespace": "AWS/ECS", + "period": 60, + "statistic": "Average", + "threshold": 45, + "treat_missing_data": "notBreaching" + } + }, + "aws_ecr_lifecycle_policy": { + "testPocketApp_ecs_service_ecr-xray-daemon_ecr-repo-lifecyclepolicy_B1867100": { + "depends_on": [ + "aws_ecr_repository.testPocketApp_ecs_service_ecr-xray-daemon_ecr-repo_9028CD2C" + ], + "policy": "{\\"rules\\":[{\\"rulePriority\\":1,\\"description\\":\\"expire old images\\",\\"selection\\":{\\"tagStatus\\":\\"any\\",\\"countType\\":\\"imageCountMoreThan\\",\\"countNumber\\":800},\\"action\\":{\\"type\\":\\"expire\\"}}]}", + "repository": "\${aws_ecr_repository.testPocketApp_ecs_service_ecr-xray-daemon_ecr-repo_9028CD2C.name}" + } + }, + "aws_ecr_repository": { + "testPocketApp_ecs_service_ecr-xray-daemon_ecr-repo_9028CD2C": { + "image_scanning_configuration": { + "scan_on_push": true + }, + "name": "testapp-xray-daemon" + } + }, + "aws_ecs_cluster": { + "testPocketApp_ecs_cluster_C3960066": { + "name": "testapp", + "setting": [ + { + "name": "containerInsights", + "value": "enabled" + } + ] + } + }, + "aws_ecs_service": { + "testPocketApp_ecs_service_ecs-service_182DEA4C": { + "cluster": "\${aws_ecs_cluster.testPocketApp_ecs_cluster_C3960066.arn}", + "depends_on": [ + "aws_ecr_repository.testPocketApp_ecs_service_ecr-xray-daemon_ecr-repo_9028CD2C" + ], + "deployment_controller": { + "type": "ECS" + }, + "deployment_maximum_percent": 200, + "deployment_minimum_healthy_percent": 100, + "desired_count": 2, + "launch_type": "FARGATE", + "lifecycle": { + "ignore_changes": [ + "desired_count", + "load_balancer" + ] + }, + "load_balancer": [ + ], + "name": "testapp", + "network_configuration": { + "security_groups": [ + "\${aws_security_group.testPocketApp_ecs_service_ecs_security_group_9C016FA8.id}" + ], + "subnets": "\${data.aws_subnets.testPocketApp_pocket_vpc_private_subnet_ids_EB1E3A65.ids}" + }, + "propagate_tags": "SERVICE", + "task_definition": "\${aws_ecs_task_definition.testPocketApp_ecs_service_ecs-task_A7E74E45.arn}" + } + }, + "aws_ecs_task_definition": { + "testPocketApp_ecs_service_ecs-task_A7E74E45": { + "container_definitions": "[{\\"dnsSearchDomains\\":null,\\"environmentFiles\\":null,\\"logConfiguration\\":{\\"logDriver\\":\\"awslogs\\",\\"secretOptions\\":[],\\"options\\":{\\"awslogs-group\\":\\"\${aws_cloudwatch_log_group.testPocketApp_ecs_service_ecs-xray-daemon_55253A74.name}\\",\\"awslogs-region\\":\\"us-east-1\\",\\"awslogs-stream-prefix\\":\\"ecs\\"}},\\"entryPoint\\":null,\\"portMappings\\":[{\\"hostPort\\":0,\\"containerPort\\":2000,\\"protocol\\":\\"udp\\"}],\\"linuxParameters\\":null,\\"cpu\\":10,\\"environment\\":[],\\"resourceRequirements\\":null,\\"ulimits\\":null,\\"repositoryCredentials\\":null,\\"dnsServers\\":null,\\"mountPoints\\":[],\\"workingDirectory\\":null,\\"secrets\\":null,\\"dockerSecurityOptions\\":null,\\"memory\\":null,\\"memoryReservation\\":50,\\"volumesFrom\\":[],\\"stopTimeout\\":null,\\"image\\":\\"\${aws_ecr_repository.testPocketApp_ecs_service_ecr-xray-daemon_ecr-repo_9028CD2C.repository_url}:latest\\",\\"startTimeout\\":null,\\"firelensConfiguration\\":null,\\"dependsOn\\":null,\\"disableNetworking\\":null,\\"interactive\\":null,\\"healthCheck\\":null,\\"essential\\":true,\\"links\\":null,\\"hostname\\":null,\\"extraHosts\\":null,\\"pseudoTerminal\\":null,\\"user\\":null,\\"readonlyRootFilesystem\\":false,\\"dockerLabels\\":null,\\"systemControls\\":null,\\"privileged\\":null,\\"name\\":\\"xray-daemon\\"}]", + "cpu": "512", + "depends_on": [ + "aws_ecr_repository.testPocketApp_ecs_service_ecr-xray-daemon_ecr-repo_9028CD2C" + ], + "execution_role_arn": "\${aws_iam_role.testPocketApp_ecs_service_ecs-iam_ecs-execution-role_EB461A0C.arn}", + "family": "testapp", + "memory": "2048", + "network_mode": "awsvpc", + "requires_compatibilities": [ + "FARGATE" + ], + "task_role_arn": "\${aws_iam_role.testPocketApp_ecs_service_ecs-iam_ecs-task-role_7DB93963.arn}", + "volume": [ + ] + } + }, + "aws_iam_role": { + "testPocketApp_ecs_service_ecs-iam_ecs-execution-role_EB461A0C": { + "assume_role_policy": "\${data.aws_iam_policy_document.testPocketApp_ecs_service_ecs-iam_ecs-task-assume_ABB81680.json}", + "name": "testapp-TaskExecutionRole" + }, + "testPocketApp_ecs_service_ecs-iam_ecs-task-role_7DB93963": { + "assume_role_policy": "\${data.aws_iam_policy_document.testPocketApp_ecs_service_ecs-iam_ecs-task-assume_ABB81680.json}", + "name": "testapp-TaskRole" + } + }, + "aws_security_group": { + "testPocketApp_ecs_service_ecs_security_group_9C016FA8": { + "description": "Internal ECS Security Group (Managed by Terraform)", + "egress": [ + { + "cidr_blocks": [ + "0.0.0.0/0" + ], + "description": "required", + "from_port": 0, + "ipv6_cidr_blocks": [ + ], + "prefix_list_ids": [ + ], + "protocol": "-1", + "security_groups": [ + ], + "self": null, + "to_port": 0 + } + ], + "ingress": [ + ], + "lifecycle": { + "create_before_destroy": true + }, + "name_prefix": "testapp-ECSSecurityGroup", + "vpc_id": "\${data.aws_vpc.testPocketApp_pocket_vpc_C4E157E3.id}" + } + } + } +}" +`; diff --git a/packages/terraform-modules/src/pocket/__snapshots__/PocketECSCodePipeline.spec.ts.snap b/packages/terraform-modules/src/pocket/__snapshots__/PocketECSCodePipeline.spec.ts.snap new file mode 100644 index 00000000..820fa555 --- /dev/null +++ b/packages/terraform-modules/src/pocket/__snapshots__/PocketECSCodePipeline.spec.ts.snap @@ -0,0 +1,2146 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`renders a Pocket ECS Codepipeline template 1`] = ` +"{ + "data": { + "aws_iam_policy_document": { + "test-codepipeline_codepipeline-assume-role-policy_AB972C21": { + "statement": [ + { + "actions": [ + "sts:AssumeRole" + ], + "effect": "Allow", + "principals": [ + { + "identifiers": [ + "codepipeline.amazonaws.com" + ], + "type": "Service" + } + ] + } + ] + }, + "test-codepipeline_codepipeline-role-policy-document_DD4EB3DB": { + "statement": [ + { + "actions": [ + "codestar-connections:UseConnection" + ], + "effect": "Allow", + "resources": [ + "arn:codestart-connection:*" + ] + }, + { + "actions": [ + "codebuild:BatchGetBuilds", + "codebuild:StartBuild", + "codebuild:BatchGetBuildBatches", + "codebuild:StartBuildBatch" + ], + "effect": "Allow", + "resources": [ + "arn:aws:codebuild:*:*:project/Test-Env*" + ] + }, + { + "actions": [ + "codedeploy:CreateDeployment", + "codedeploy:GetApplication", + "codedeploy:GetApplicationRevision", + "codedeploy:GetDeployment", + "codedeploy:RegisterApplicationRevision", + "codedeploy:GetDeploymentConfig" + ], + "effect": "Allow", + "resources": [ + "arn:aws:codedeploy:*:*:application:Test-Env-ECS", + "arn:aws:codedeploy:*:*:deploymentgroup:Test-Env-ECS/Test-Env-ECS", + "arn:aws:codedeploy:*:*:deploymentconfig:*" + ] + }, + { + "actions": [ + "s3:GetObject", + "s3:GetObjectVersion", + "s3:GetBucketVersioning", + "s3:PutObjectAcl", + "s3:PutObject" + ], + "effect": "Allow", + "resources": [ + "\${aws_s3_bucket.test-codepipeline_codepipeline-bucket_D4671464.arn}", + "\${aws_s3_bucket.test-codepipeline_codepipeline-bucket_D4671464.arn}/*" + ] + }, + { + "actions": [ + "iam:PassRole" + ], + "condition": [ + { + "test": "StringEqualsIfExists", + "values": [ + "ecs-tasks.amazonaws.com" + ], + "variable": "iam:PassedToService" + } + ], + "effect": "Allow", + "resources": [ + "*" + ] + }, + { + "actions": [ + "ecs:RegisterTaskDefinition" + ], + "effect": "Allow", + "resources": [ + "*" + ] + } + ] + } + }, + "aws_kms_alias": { + "test-codepipeline_kms_s3_alias_6C922DC1": { + "name": "alias/aws/s3" + } + } + }, + "resource": { + "aws_codepipeline": { + "test-codepipeline_367BF1E2": { + "artifact_store": [ + { + "encryption_key": { + "id": "\${data.aws_kms_alias.test-codepipeline_kms_s3_alias_6C922DC1.arn}", + "type": "KMS" + }, + "location": "\${aws_s3_bucket.test-codepipeline_codepipeline-bucket_D4671464.bucket}", + "type": "S3" + } + ], + "name": "Test-Env-CodePipeline", + "role_arn": "\${aws_iam_role.test-codepipeline_codepipeline-role_4D3D2364.arn}", + "stage": [ + { + "action": [ + { + "category": "Source", + "configuration": { + "BranchName": "test", + "ConnectionArn": "arn:codestart-connection:*", + "DetectChanges": "false", + "FullRepositoryId": "string" + }, + "name": "GitHub_Checkout", + "namespace": "SourceVariables", + "output_artifacts": [ + "SourceOutput" + ], + "owner": "AWS", + "provider": "CodeStarSourceConnection", + "version": "1" + } + ], + "name": "Source" + }, + { + "action": [ + { + "category": "Build", + "configuration": { + "EnvironmentVariables": "[{\\"name\\":\\"GIT_BRANCH\\",\\"value\\":\\"#{SourceVariables.BranchName}\\"}]", + "ProjectName": "Test-Env" + }, + "input_artifacts": [ + "SourceOutput" + ], + "name": "Deploy_CDK", + "output_artifacts": [ + "CodeBuildOutput" + ], + "owner": "AWS", + "provider": "CodeBuild", + "run_order": 1, + "version": "1" + }, + { + "category": "Deploy", + "configuration": { + "AppSpecTemplateArtifact": "CodeBuildOutput", + "AppSpecTemplatePath": "appspec.json", + "ApplicationName": "Test-Env-ECS", + "DeploymentGroupName": "Test-Env-ECS", + "TaskDefinitionTemplateArtifact": "CodeBuildOutput", + "TaskDefinitionTemplatePath": "taskdef.json" + }, + "input_artifacts": [ + "CodeBuildOutput" + ], + "name": "Deploy_ECS", + "owner": "AWS", + "provider": "CodeDeployToECS", + "run_order": 2, + "version": "1" + } + ], + "name": "Deploy" + } + ] + } + }, + "aws_iam_role": { + "test-codepipeline_codepipeline-role_4D3D2364": { + "assume_role_policy": "\${data.aws_iam_policy_document.test-codepipeline_codepipeline-assume-role-policy_AB972C21.json}", + "name": "Test-Env-CodePipelineRole" + } + }, + "aws_iam_role_policy": { + "test-codepipeline_codepipeline-role-policy_76FC5FBB": { + "name": "Test-Env-CodePipeline-Role-Policy", + "policy": "\${data.aws_iam_policy_document.test-codepipeline_codepipeline-role-policy-document_DD4EB3DB.json}", + "role": "\${aws_iam_role.test-codepipeline_codepipeline-role_4D3D2364.id}" + } + }, + "aws_s3_bucket": { + "test-codepipeline_codepipeline-bucket_D4671464": { + "bucket": "pocket-codepipeline-e7e6d80d49c8787d2f7a7aca123082dd", + "force_destroy": true + } + }, + "aws_s3_bucket_acl": { + "test-codepipeline_code-bucket-acl_52349EA5": { + "acl": "private", + "bucket": "\${aws_s3_bucket.test-codepipeline_codepipeline-bucket_D4671464.id}", + "depends_on": [ + "aws_s3_bucket_ownership_controls.test-codepipeline_code-bucket-ownership-controls_737E4D7F" + ] + } + }, + "aws_s3_bucket_ownership_controls": { + "test-codepipeline_code-bucket-ownership-controls_737E4D7F": { + "bucket": "\${aws_s3_bucket.test-codepipeline_codepipeline-bucket_D4671464.id}", + "rule": { + "object_ownership": "BucketOwnerPreferred" + } + } + } + } +}" +`; + +exports[`renders a Pocket ECS Codepipeline template with custom steps 1`] = ` +"{ + "data": { + "aws_iam_policy_document": { + "test-codepipeline_codepipeline-assume-role-policy_AB972C21": { + "statement": [ + { + "actions": [ + "sts:AssumeRole" + ], + "effect": "Allow", + "principals": [ + { + "identifiers": [ + "codepipeline.amazonaws.com" + ], + "type": "Service" + } + ] + } + ] + }, + "test-codepipeline_codepipeline-role-policy-document_DD4EB3DB": { + "statement": [ + { + "actions": [ + "codestar-connections:UseConnection" + ], + "effect": "Allow", + "resources": [ + "arn:codestart-connection:*" + ] + }, + { + "actions": [ + "codebuild:BatchGetBuilds", + "codebuild:StartBuild", + "codebuild:BatchGetBuildBatches", + "codebuild:StartBuildBatch" + ], + "effect": "Allow", + "resources": [ + "arn:aws:codebuild:*:*:project/Test-Env*" + ] + }, + { + "actions": [ + "codedeploy:CreateDeployment", + "codedeploy:GetApplication", + "codedeploy:GetApplicationRevision", + "codedeploy:GetDeployment", + "codedeploy:RegisterApplicationRevision", + "codedeploy:GetDeploymentConfig" + ], + "effect": "Allow", + "resources": [ + "arn:aws:codedeploy:*:*:application:Test-Env-ECS", + "arn:aws:codedeploy:*:*:deploymentgroup:Test-Env-ECS/Test-Env-ECS", + "arn:aws:codedeploy:*:*:deploymentconfig:*" + ] + }, + { + "actions": [ + "s3:GetObject", + "s3:GetObjectVersion", + "s3:GetBucketVersioning", + "s3:PutObjectAcl", + "s3:PutObject" + ], + "effect": "Allow", + "resources": [ + "\${aws_s3_bucket.test-codepipeline_codepipeline-bucket_D4671464.arn}", + "\${aws_s3_bucket.test-codepipeline_codepipeline-bucket_D4671464.arn}/*" + ] + }, + { + "actions": [ + "iam:PassRole" + ], + "condition": [ + { + "test": "StringEqualsIfExists", + "values": [ + "ecs-tasks.amazonaws.com" + ], + "variable": "iam:PassedToService" + } + ], + "effect": "Allow", + "resources": [ + "*" + ] + }, + { + "actions": [ + "ecs:RegisterTaskDefinition" + ], + "effect": "Allow", + "resources": [ + "*" + ] + } + ] + } + }, + "aws_kms_alias": { + "test-codepipeline_kms_s3_alias_6C922DC1": { + "name": "alias/aws/s3" + } + } + }, + "resource": { + "aws_codepipeline": { + "test-codepipeline_367BF1E2": { + "artifact_store": [ + { + "encryption_key": { + "id": "\${data.aws_kms_alias.test-codepipeline_kms_s3_alias_6C922DC1.arn}", + "type": "KMS" + }, + "location": "\${aws_s3_bucket.test-codepipeline_codepipeline-bucket_D4671464.bucket}", + "type": "S3" + } + ], + "name": "Test-Env-CodePipeline", + "role_arn": "\${aws_iam_role.test-codepipeline_codepipeline-role_4D3D2364.arn}", + "stage": [ + { + "action": [ + { + "category": "Source", + "configuration": { + "BranchName": "test", + "ConnectionArn": "arn:codestart-connection:*", + "DetectChanges": "false", + "FullRepositoryId": "string" + }, + "name": "GitHub_Checkout", + "namespace": "SourceVariables", + "output_artifacts": [ + "SourceOutput" + ], + "owner": "AWS", + "provider": "CodeStarSourceConnection", + "version": "1" + } + ], + "name": "Source" + }, + { + "action": [ + { + "category": "Deploy", + "configuration": { + "projectName": "Test-Env-MyBuild" + }, + "name": "MyBuild", + "owner": "AWS", + "provider": "CodeBuild", + "run_order": 1, + "version": "1" + } + ], + "name": "Custom_PreDeploy_Stage" + }, + { + "action": [ + { + "category": "Build", + "configuration": { + "EnvironmentVariables": "[{\\"name\\":\\"GIT_BRANCH\\",\\"value\\":\\"#{SourceVariables.BranchName}\\"}]", + "ProjectName": "Test-Env" + }, + "input_artifacts": [ + "SourceOutput" + ], + "name": "Deploy_CDK", + "output_artifacts": [ + "CodeBuildOutput" + ], + "owner": "AWS", + "provider": "CodeBuild", + "run_order": 1, + "version": "1" + }, + { + "category": "Deploy", + "configuration": { + "AppSpecTemplateArtifact": "CodeBuildOutput", + "AppSpecTemplatePath": "appspec.json", + "ApplicationName": "Test-Env-ECS", + "DeploymentGroupName": "Test-Env-ECS", + "TaskDefinitionTemplateArtifact": "CodeBuildOutput", + "TaskDefinitionTemplatePath": "taskdef.json" + }, + "input_artifacts": [ + "CodeBuildOutput" + ], + "name": "Deploy_ECS", + "owner": "AWS", + "provider": "CodeDeployToECS", + "run_order": 2, + "version": "1" + } + ], + "name": "Deploy" + }, + { + "action": [ + { + "category": "Deploy", + "configuration": { + "ExecutionNamePrefix": "CodePipelineDeploy", + "stateMachineArn": "myStateMachineArn123" + }, + "name": "Custom_Action", + "owner": "AWS", + "provider": "StepFunctions", + "run_order": 1, + "version": "1" + } + ], + "name": "Custom_PostDeploy_Stage" + } + ] + } + }, + "aws_iam_role": { + "test-codepipeline_codepipeline-role_4D3D2364": { + "assume_role_policy": "\${data.aws_iam_policy_document.test-codepipeline_codepipeline-assume-role-policy_AB972C21.json}", + "name": "Test-Env-CodePipelineRole" + } + }, + "aws_iam_role_policy": { + "test-codepipeline_codepipeline-role-policy_76FC5FBB": { + "name": "Test-Env-CodePipeline-Role-Policy", + "policy": "\${data.aws_iam_policy_document.test-codepipeline_codepipeline-role-policy-document_DD4EB3DB.json}", + "role": "\${aws_iam_role.test-codepipeline_codepipeline-role_4D3D2364.id}" + } + }, + "aws_s3_bucket": { + "test-codepipeline_codepipeline-bucket_D4671464": { + "bucket": "pocket-codepipeline-e7e6d80d49c8787d2f7a7aca123082dd", + "force_destroy": true + } + }, + "aws_s3_bucket_acl": { + "test-codepipeline_code-bucket-acl_52349EA5": { + "acl": "private", + "bucket": "\${aws_s3_bucket.test-codepipeline_codepipeline-bucket_D4671464.id}", + "depends_on": [ + "aws_s3_bucket_ownership_controls.test-codepipeline_code-bucket-ownership-controls_737E4D7F" + ] + } + }, + "aws_s3_bucket_ownership_controls": { + "test-codepipeline_code-bucket-ownership-controls_737E4D7F": { + "bucket": "\${aws_s3_bucket.test-codepipeline_codepipeline-bucket_D4671464.id}", + "rule": { + "object_ownership": "BucketOwnerPreferred" + } + } + } + } +}" +`; + +exports[`renders a Pocket ECS Codepipeline template with tags 1`] = ` +"{ + "data": { + "aws_iam_policy_document": { + "test-codepipeline_codepipeline-assume-role-policy_AB972C21": { + "statement": [ + { + "actions": [ + "sts:AssumeRole" + ], + "effect": "Allow", + "principals": [ + { + "identifiers": [ + "codepipeline.amazonaws.com" + ], + "type": "Service" + } + ] + } + ] + }, + "test-codepipeline_codepipeline-role-policy-document_DD4EB3DB": { + "statement": [ + { + "actions": [ + "codestar-connections:UseConnection" + ], + "effect": "Allow", + "resources": [ + "arn:codestart-connection:*" + ] + }, + { + "actions": [ + "codebuild:BatchGetBuilds", + "codebuild:StartBuild", + "codebuild:BatchGetBuildBatches", + "codebuild:StartBuildBatch" + ], + "effect": "Allow", + "resources": [ + "arn:aws:codebuild:*:*:project/Test-Env*" + ] + }, + { + "actions": [ + "codedeploy:CreateDeployment", + "codedeploy:GetApplication", + "codedeploy:GetApplicationRevision", + "codedeploy:GetDeployment", + "codedeploy:RegisterApplicationRevision", + "codedeploy:GetDeploymentConfig" + ], + "effect": "Allow", + "resources": [ + "arn:aws:codedeploy:*:*:application:Test-Env-ECS", + "arn:aws:codedeploy:*:*:deploymentgroup:Test-Env-ECS/Test-Env-ECS", + "arn:aws:codedeploy:*:*:deploymentconfig:*" + ] + }, + { + "actions": [ + "s3:GetObject", + "s3:GetObjectVersion", + "s3:GetBucketVersioning", + "s3:PutObjectAcl", + "s3:PutObject" + ], + "effect": "Allow", + "resources": [ + "\${aws_s3_bucket.test-codepipeline_codepipeline-bucket_D4671464.arn}", + "\${aws_s3_bucket.test-codepipeline_codepipeline-bucket_D4671464.arn}/*" + ] + }, + { + "actions": [ + "iam:PassRole" + ], + "condition": [ + { + "test": "StringEqualsIfExists", + "values": [ + "ecs-tasks.amazonaws.com" + ], + "variable": "iam:PassedToService" + } + ], + "effect": "Allow", + "resources": [ + "*" + ] + }, + { + "actions": [ + "ecs:RegisterTaskDefinition" + ], + "effect": "Allow", + "resources": [ + "*" + ] + } + ] + } + }, + "aws_kms_alias": { + "test-codepipeline_kms_s3_alias_6C922DC1": { + "name": "alias/aws/s3" + } + } + }, + "resource": { + "aws_codepipeline": { + "test-codepipeline_367BF1E2": { + "artifact_store": [ + { + "encryption_key": { + "id": "\${data.aws_kms_alias.test-codepipeline_kms_s3_alias_6C922DC1.arn}", + "type": "KMS" + }, + "location": "\${aws_s3_bucket.test-codepipeline_codepipeline-bucket_D4671464.bucket}", + "type": "S3" + } + ], + "name": "Test-Env-CodePipeline", + "role_arn": "\${aws_iam_role.test-codepipeline_codepipeline-role_4D3D2364.arn}", + "stage": [ + { + "action": [ + { + "category": "Source", + "configuration": { + "BranchName": "test", + "ConnectionArn": "arn:codestart-connection:*", + "DetectChanges": "false", + "FullRepositoryId": "string" + }, + "name": "GitHub_Checkout", + "namespace": "SourceVariables", + "output_artifacts": [ + "SourceOutput" + ], + "owner": "AWS", + "provider": "CodeStarSourceConnection", + "version": "1" + } + ], + "name": "Source" + }, + { + "action": [ + { + "category": "Build", + "configuration": { + "EnvironmentVariables": "[{\\"name\\":\\"GIT_BRANCH\\",\\"value\\":\\"#{SourceVariables.BranchName}\\"}]", + "ProjectName": "Test-Env" + }, + "input_artifacts": [ + "SourceOutput" + ], + "name": "Deploy_CDK", + "output_artifacts": [ + "CodeBuildOutput" + ], + "owner": "AWS", + "provider": "CodeBuild", + "run_order": 1, + "version": "1" + }, + { + "category": "Deploy", + "configuration": { + "AppSpecTemplateArtifact": "CodeBuildOutput", + "AppSpecTemplatePath": "appspec.json", + "ApplicationName": "Test-Env-ECS", + "DeploymentGroupName": "Test-Env-ECS", + "TaskDefinitionTemplateArtifact": "CodeBuildOutput", + "TaskDefinitionTemplatePath": "taskdef.json" + }, + "input_artifacts": [ + "CodeBuildOutput" + ], + "name": "Deploy_ECS", + "owner": "AWS", + "provider": "CodeDeployToECS", + "run_order": 2, + "version": "1" + } + ], + "name": "Deploy" + } + ], + "tags": { + "yup": "a tag" + } + } + }, + "aws_iam_role": { + "test-codepipeline_codepipeline-role_4D3D2364": { + "assume_role_policy": "\${data.aws_iam_policy_document.test-codepipeline_codepipeline-assume-role-policy_AB972C21.json}", + "name": "Test-Env-CodePipelineRole" + } + }, + "aws_iam_role_policy": { + "test-codepipeline_codepipeline-role-policy_76FC5FBB": { + "name": "Test-Env-CodePipeline-Role-Policy", + "policy": "\${data.aws_iam_policy_document.test-codepipeline_codepipeline-role-policy-document_DD4EB3DB.json}", + "role": "\${aws_iam_role.test-codepipeline_codepipeline-role_4D3D2364.id}" + } + }, + "aws_s3_bucket": { + "test-codepipeline_codepipeline-bucket_D4671464": { + "bucket": "pocket-codepipeline-e7e6d80d49c8787d2f7a7aca123082dd", + "force_destroy": true, + "tags": { + "yup": "a tag" + } + } + }, + "aws_s3_bucket_acl": { + "test-codepipeline_code-bucket-acl_52349EA5": { + "acl": "private", + "bucket": "\${aws_s3_bucket.test-codepipeline_codepipeline-bucket_D4671464.id}", + "depends_on": [ + "aws_s3_bucket_ownership_controls.test-codepipeline_code-bucket-ownership-controls_737E4D7F" + ] + } + }, + "aws_s3_bucket_ownership_controls": { + "test-codepipeline_code-bucket-ownership-controls_737E4D7F": { + "bucket": "\${aws_s3_bucket.test-codepipeline_codepipeline-bucket_D4671464.id}", + "rule": { + "object_ownership": "BucketOwnerPreferred" + } + } + } + } +}" +`; + +exports[`renders a Pocket ECS Codepipeline template with the provided CodeDeploy app name 1`] = ` +"{ + "data": { + "aws_iam_policy_document": { + "test-codepipeline_codepipeline-assume-role-policy_AB972C21": { + "statement": [ + { + "actions": [ + "sts:AssumeRole" + ], + "effect": "Allow", + "principals": [ + { + "identifiers": [ + "codepipeline.amazonaws.com" + ], + "type": "Service" + } + ] + } + ] + }, + "test-codepipeline_codepipeline-role-policy-document_DD4EB3DB": { + "statement": [ + { + "actions": [ + "codestar-connections:UseConnection" + ], + "effect": "Allow", + "resources": [ + "arn:codestart-connection:*" + ] + }, + { + "actions": [ + "codebuild:BatchGetBuilds", + "codebuild:StartBuild", + "codebuild:BatchGetBuildBatches", + "codebuild:StartBuildBatch" + ], + "effect": "Allow", + "resources": [ + "arn:aws:codebuild:*:*:project/Test-Env*" + ] + }, + { + "actions": [ + "codedeploy:CreateDeployment", + "codedeploy:GetApplication", + "codedeploy:GetApplicationRevision", + "codedeploy:GetDeployment", + "codedeploy:RegisterApplicationRevision", + "codedeploy:GetDeploymentConfig" + ], + "effect": "Allow", + "resources": [ + "arn:aws:codedeploy:*:*:application:TestAppName", + "arn:aws:codedeploy:*:*:deploymentgroup:TestAppName/Test-Env-ECS", + "arn:aws:codedeploy:*:*:deploymentconfig:*" + ] + }, + { + "actions": [ + "s3:GetObject", + "s3:GetObjectVersion", + "s3:GetBucketVersioning", + "s3:PutObjectAcl", + "s3:PutObject" + ], + "effect": "Allow", + "resources": [ + "\${aws_s3_bucket.test-codepipeline_codepipeline-bucket_D4671464.arn}", + "\${aws_s3_bucket.test-codepipeline_codepipeline-bucket_D4671464.arn}/*" + ] + }, + { + "actions": [ + "iam:PassRole" + ], + "condition": [ + { + "test": "StringEqualsIfExists", + "values": [ + "ecs-tasks.amazonaws.com" + ], + "variable": "iam:PassedToService" + } + ], + "effect": "Allow", + "resources": [ + "*" + ] + }, + { + "actions": [ + "ecs:RegisterTaskDefinition" + ], + "effect": "Allow", + "resources": [ + "*" + ] + } + ] + } + }, + "aws_kms_alias": { + "test-codepipeline_kms_s3_alias_6C922DC1": { + "name": "alias/aws/s3" + } + } + }, + "resource": { + "aws_codepipeline": { + "test-codepipeline_367BF1E2": { + "artifact_store": [ + { + "encryption_key": { + "id": "\${data.aws_kms_alias.test-codepipeline_kms_s3_alias_6C922DC1.arn}", + "type": "KMS" + }, + "location": "\${aws_s3_bucket.test-codepipeline_codepipeline-bucket_D4671464.bucket}", + "type": "S3" + } + ], + "name": "Test-Env-CodePipeline", + "role_arn": "\${aws_iam_role.test-codepipeline_codepipeline-role_4D3D2364.arn}", + "stage": [ + { + "action": [ + { + "category": "Source", + "configuration": { + "BranchName": "test", + "ConnectionArn": "arn:codestart-connection:*", + "DetectChanges": "false", + "FullRepositoryId": "string" + }, + "name": "GitHub_Checkout", + "namespace": "SourceVariables", + "output_artifacts": [ + "SourceOutput" + ], + "owner": "AWS", + "provider": "CodeStarSourceConnection", + "version": "1" + } + ], + "name": "Source" + }, + { + "action": [ + { + "category": "Build", + "configuration": { + "EnvironmentVariables": "[{\\"name\\":\\"GIT_BRANCH\\",\\"value\\":\\"#{SourceVariables.BranchName}\\"}]", + "ProjectName": "Test-Env" + }, + "input_artifacts": [ + "SourceOutput" + ], + "name": "Deploy_CDK", + "output_artifacts": [ + "CodeBuildOutput" + ], + "owner": "AWS", + "provider": "CodeBuild", + "run_order": 1, + "version": "1" + }, + { + "category": "Deploy", + "configuration": { + "AppSpecTemplateArtifact": "CodeBuildOutput", + "AppSpecTemplatePath": "appspec.json", + "ApplicationName": "TestAppName", + "DeploymentGroupName": "Test-Env-ECS", + "TaskDefinitionTemplateArtifact": "CodeBuildOutput", + "TaskDefinitionTemplatePath": "taskdef.json" + }, + "input_artifacts": [ + "CodeBuildOutput" + ], + "name": "Deploy_ECS", + "owner": "AWS", + "provider": "CodeDeployToECS", + "run_order": 2, + "version": "1" + } + ], + "name": "Deploy" + } + ] + } + }, + "aws_iam_role": { + "test-codepipeline_codepipeline-role_4D3D2364": { + "assume_role_policy": "\${data.aws_iam_policy_document.test-codepipeline_codepipeline-assume-role-policy_AB972C21.json}", + "name": "Test-Env-CodePipelineRole" + } + }, + "aws_iam_role_policy": { + "test-codepipeline_codepipeline-role-policy_76FC5FBB": { + "name": "Test-Env-CodePipeline-Role-Policy", + "policy": "\${data.aws_iam_policy_document.test-codepipeline_codepipeline-role-policy-document_DD4EB3DB.json}", + "role": "\${aws_iam_role.test-codepipeline_codepipeline-role_4D3D2364.id}" + } + }, + "aws_s3_bucket": { + "test-codepipeline_codepipeline-bucket_D4671464": { + "bucket": "pocket-codepipeline-e7e6d80d49c8787d2f7a7aca123082dd", + "force_destroy": true + } + }, + "aws_s3_bucket_acl": { + "test-codepipeline_code-bucket-acl_52349EA5": { + "acl": "private", + "bucket": "\${aws_s3_bucket.test-codepipeline_codepipeline-bucket_D4671464.id}", + "depends_on": [ + "aws_s3_bucket_ownership_controls.test-codepipeline_code-bucket-ownership-controls_737E4D7F" + ] + } + }, + "aws_s3_bucket_ownership_controls": { + "test-codepipeline_code-bucket-ownership-controls_737E4D7F": { + "bucket": "\${aws_s3_bucket.test-codepipeline_codepipeline-bucket_D4671464.id}", + "rule": { + "object_ownership": "BucketOwnerPreferred" + } + } + } + } +}" +`; + +exports[`renders a Pocket ECS Codepipeline template with the provided CodeDeploy deployment group name 1`] = ` +"{ + "data": { + "aws_iam_policy_document": { + "test-codepipeline_codepipeline-assume-role-policy_AB972C21": { + "statement": [ + { + "actions": [ + "sts:AssumeRole" + ], + "effect": "Allow", + "principals": [ + { + "identifiers": [ + "codepipeline.amazonaws.com" + ], + "type": "Service" + } + ] + } + ] + }, + "test-codepipeline_codepipeline-role-policy-document_DD4EB3DB": { + "statement": [ + { + "actions": [ + "codestar-connections:UseConnection" + ], + "effect": "Allow", + "resources": [ + "arn:codestart-connection:*" + ] + }, + { + "actions": [ + "codebuild:BatchGetBuilds", + "codebuild:StartBuild", + "codebuild:BatchGetBuildBatches", + "codebuild:StartBuildBatch" + ], + "effect": "Allow", + "resources": [ + "arn:aws:codebuild:*:*:project/Test-Env*" + ] + }, + { + "actions": [ + "codedeploy:CreateDeployment", + "codedeploy:GetApplication", + "codedeploy:GetApplicationRevision", + "codedeploy:GetDeployment", + "codedeploy:RegisterApplicationRevision", + "codedeploy:GetDeploymentConfig" + ], + "effect": "Allow", + "resources": [ + "arn:aws:codedeploy:*:*:application:Test-Env-ECS", + "arn:aws:codedeploy:*:*:deploymentgroup:Test-Env-ECS/TestDeploymentGroupName", + "arn:aws:codedeploy:*:*:deploymentconfig:*" + ] + }, + { + "actions": [ + "s3:GetObject", + "s3:GetObjectVersion", + "s3:GetBucketVersioning", + "s3:PutObjectAcl", + "s3:PutObject" + ], + "effect": "Allow", + "resources": [ + "\${aws_s3_bucket.test-codepipeline_codepipeline-bucket_D4671464.arn}", + "\${aws_s3_bucket.test-codepipeline_codepipeline-bucket_D4671464.arn}/*" + ] + }, + { + "actions": [ + "iam:PassRole" + ], + "condition": [ + { + "test": "StringEqualsIfExists", + "values": [ + "ecs-tasks.amazonaws.com" + ], + "variable": "iam:PassedToService" + } + ], + "effect": "Allow", + "resources": [ + "*" + ] + }, + { + "actions": [ + "ecs:RegisterTaskDefinition" + ], + "effect": "Allow", + "resources": [ + "*" + ] + } + ] + } + }, + "aws_kms_alias": { + "test-codepipeline_kms_s3_alias_6C922DC1": { + "name": "alias/aws/s3" + } + } + }, + "resource": { + "aws_codepipeline": { + "test-codepipeline_367BF1E2": { + "artifact_store": [ + { + "encryption_key": { + "id": "\${data.aws_kms_alias.test-codepipeline_kms_s3_alias_6C922DC1.arn}", + "type": "KMS" + }, + "location": "\${aws_s3_bucket.test-codepipeline_codepipeline-bucket_D4671464.bucket}", + "type": "S3" + } + ], + "name": "Test-Env-CodePipeline", + "role_arn": "\${aws_iam_role.test-codepipeline_codepipeline-role_4D3D2364.arn}", + "stage": [ + { + "action": [ + { + "category": "Source", + "configuration": { + "BranchName": "test", + "ConnectionArn": "arn:codestart-connection:*", + "DetectChanges": "false", + "FullRepositoryId": "string" + }, + "name": "GitHub_Checkout", + "namespace": "SourceVariables", + "output_artifacts": [ + "SourceOutput" + ], + "owner": "AWS", + "provider": "CodeStarSourceConnection", + "version": "1" + } + ], + "name": "Source" + }, + { + "action": [ + { + "category": "Build", + "configuration": { + "EnvironmentVariables": "[{\\"name\\":\\"GIT_BRANCH\\",\\"value\\":\\"#{SourceVariables.BranchName}\\"}]", + "ProjectName": "Test-Env" + }, + "input_artifacts": [ + "SourceOutput" + ], + "name": "Deploy_CDK", + "output_artifacts": [ + "CodeBuildOutput" + ], + "owner": "AWS", + "provider": "CodeBuild", + "run_order": 1, + "version": "1" + }, + { + "category": "Deploy", + "configuration": { + "AppSpecTemplateArtifact": "CodeBuildOutput", + "AppSpecTemplatePath": "appspec.json", + "ApplicationName": "Test-Env-ECS", + "DeploymentGroupName": "TestDeploymentGroupName", + "TaskDefinitionTemplateArtifact": "CodeBuildOutput", + "TaskDefinitionTemplatePath": "taskdef.json" + }, + "input_artifacts": [ + "CodeBuildOutput" + ], + "name": "Deploy_ECS", + "owner": "AWS", + "provider": "CodeDeployToECS", + "run_order": 2, + "version": "1" + } + ], + "name": "Deploy" + } + ] + } + }, + "aws_iam_role": { + "test-codepipeline_codepipeline-role_4D3D2364": { + "assume_role_policy": "\${data.aws_iam_policy_document.test-codepipeline_codepipeline-assume-role-policy_AB972C21.json}", + "name": "Test-Env-CodePipelineRole" + } + }, + "aws_iam_role_policy": { + "test-codepipeline_codepipeline-role-policy_76FC5FBB": { + "name": "Test-Env-CodePipeline-Role-Policy", + "policy": "\${data.aws_iam_policy_document.test-codepipeline_codepipeline-role-policy-document_DD4EB3DB.json}", + "role": "\${aws_iam_role.test-codepipeline_codepipeline-role_4D3D2364.id}" + } + }, + "aws_s3_bucket": { + "test-codepipeline_codepipeline-bucket_D4671464": { + "bucket": "pocket-codepipeline-e7e6d80d49c8787d2f7a7aca123082dd", + "force_destroy": true + } + }, + "aws_s3_bucket_acl": { + "test-codepipeline_code-bucket-acl_52349EA5": { + "acl": "private", + "bucket": "\${aws_s3_bucket.test-codepipeline_codepipeline-bucket_D4671464.id}", + "depends_on": [ + "aws_s3_bucket_ownership_controls.test-codepipeline_code-bucket-ownership-controls_737E4D7F" + ] + } + }, + "aws_s3_bucket_ownership_controls": { + "test-codepipeline_code-bucket-ownership-controls_737E4D7F": { + "bucket": "\${aws_s3_bucket.test-codepipeline_codepipeline-bucket_D4671464.id}", + "rule": { + "object_ownership": "BucketOwnerPreferred" + } + } + } + } +}" +`; + +exports[`renders a Pocket ECS Codepipeline template with the provided appspec path 1`] = ` +"{ + "data": { + "aws_iam_policy_document": { + "test-codepipeline_codepipeline-assume-role-policy_AB972C21": { + "statement": [ + { + "actions": [ + "sts:AssumeRole" + ], + "effect": "Allow", + "principals": [ + { + "identifiers": [ + "codepipeline.amazonaws.com" + ], + "type": "Service" + } + ] + } + ] + }, + "test-codepipeline_codepipeline-role-policy-document_DD4EB3DB": { + "statement": [ + { + "actions": [ + "codestar-connections:UseConnection" + ], + "effect": "Allow", + "resources": [ + "arn:codestart-connection:*" + ] + }, + { + "actions": [ + "codebuild:BatchGetBuilds", + "codebuild:StartBuild", + "codebuild:BatchGetBuildBatches", + "codebuild:StartBuildBatch" + ], + "effect": "Allow", + "resources": [ + "arn:aws:codebuild:*:*:project/Test-Env*" + ] + }, + { + "actions": [ + "codedeploy:CreateDeployment", + "codedeploy:GetApplication", + "codedeploy:GetApplicationRevision", + "codedeploy:GetDeployment", + "codedeploy:RegisterApplicationRevision", + "codedeploy:GetDeploymentConfig" + ], + "effect": "Allow", + "resources": [ + "arn:aws:codedeploy:*:*:application:Test-Env-ECS", + "arn:aws:codedeploy:*:*:deploymentgroup:Test-Env-ECS/Test-Env-ECS", + "arn:aws:codedeploy:*:*:deploymentconfig:*" + ] + }, + { + "actions": [ + "s3:GetObject", + "s3:GetObjectVersion", + "s3:GetBucketVersioning", + "s3:PutObjectAcl", + "s3:PutObject" + ], + "effect": "Allow", + "resources": [ + "\${aws_s3_bucket.test-codepipeline_codepipeline-bucket_D4671464.arn}", + "\${aws_s3_bucket.test-codepipeline_codepipeline-bucket_D4671464.arn}/*" + ] + }, + { + "actions": [ + "iam:PassRole" + ], + "condition": [ + { + "test": "StringEqualsIfExists", + "values": [ + "ecs-tasks.amazonaws.com" + ], + "variable": "iam:PassedToService" + } + ], + "effect": "Allow", + "resources": [ + "*" + ] + }, + { + "actions": [ + "ecs:RegisterTaskDefinition" + ], + "effect": "Allow", + "resources": [ + "*" + ] + } + ] + } + }, + "aws_kms_alias": { + "test-codepipeline_kms_s3_alias_6C922DC1": { + "name": "alias/aws/s3" + } + } + }, + "resource": { + "aws_codepipeline": { + "test-codepipeline_367BF1E2": { + "artifact_store": [ + { + "encryption_key": { + "id": "\${data.aws_kms_alias.test-codepipeline_kms_s3_alias_6C922DC1.arn}", + "type": "KMS" + }, + "location": "\${aws_s3_bucket.test-codepipeline_codepipeline-bucket_D4671464.bucket}", + "type": "S3" + } + ], + "name": "Test-Env-CodePipeline", + "role_arn": "\${aws_iam_role.test-codepipeline_codepipeline-role_4D3D2364.arn}", + "stage": [ + { + "action": [ + { + "category": "Source", + "configuration": { + "BranchName": "test", + "ConnectionArn": "arn:codestart-connection:*", + "DetectChanges": "false", + "FullRepositoryId": "string" + }, + "name": "GitHub_Checkout", + "namespace": "SourceVariables", + "output_artifacts": [ + "SourceOutput" + ], + "owner": "AWS", + "provider": "CodeStarSourceConnection", + "version": "1" + } + ], + "name": "Source" + }, + { + "action": [ + { + "category": "Build", + "configuration": { + "EnvironmentVariables": "[{\\"name\\":\\"GIT_BRANCH\\",\\"value\\":\\"#{SourceVariables.BranchName}\\"}]", + "ProjectName": "Test-Env" + }, + "input_artifacts": [ + "SourceOutput" + ], + "name": "Deploy_CDK", + "output_artifacts": [ + "CodeBuildOutput" + ], + "owner": "AWS", + "provider": "CodeBuild", + "run_order": 1, + "version": "1" + }, + { + "category": "Deploy", + "configuration": { + "AppSpecTemplateArtifact": "CodeBuildOutput", + "AppSpecTemplatePath": "testappspec.json", + "ApplicationName": "Test-Env-ECS", + "DeploymentGroupName": "Test-Env-ECS", + "TaskDefinitionTemplateArtifact": "CodeBuildOutput", + "TaskDefinitionTemplatePath": "taskdef.json" + }, + "input_artifacts": [ + "CodeBuildOutput" + ], + "name": "Deploy_ECS", + "owner": "AWS", + "provider": "CodeDeployToECS", + "run_order": 2, + "version": "1" + } + ], + "name": "Deploy" + } + ] + } + }, + "aws_iam_role": { + "test-codepipeline_codepipeline-role_4D3D2364": { + "assume_role_policy": "\${data.aws_iam_policy_document.test-codepipeline_codepipeline-assume-role-policy_AB972C21.json}", + "name": "Test-Env-CodePipelineRole" + } + }, + "aws_iam_role_policy": { + "test-codepipeline_codepipeline-role-policy_76FC5FBB": { + "name": "Test-Env-CodePipeline-Role-Policy", + "policy": "\${data.aws_iam_policy_document.test-codepipeline_codepipeline-role-policy-document_DD4EB3DB.json}", + "role": "\${aws_iam_role.test-codepipeline_codepipeline-role_4D3D2364.id}" + } + }, + "aws_s3_bucket": { + "test-codepipeline_codepipeline-bucket_D4671464": { + "bucket": "pocket-codepipeline-e7e6d80d49c8787d2f7a7aca123082dd", + "force_destroy": true + } + }, + "aws_s3_bucket_acl": { + "test-codepipeline_code-bucket-acl_52349EA5": { + "acl": "private", + "bucket": "\${aws_s3_bucket.test-codepipeline_codepipeline-bucket_D4671464.id}", + "depends_on": [ + "aws_s3_bucket_ownership_controls.test-codepipeline_code-bucket-ownership-controls_737E4D7F" + ] + } + }, + "aws_s3_bucket_ownership_controls": { + "test-codepipeline_code-bucket-ownership-controls_737E4D7F": { + "bucket": "\${aws_s3_bucket.test-codepipeline_codepipeline-bucket_D4671464.id}", + "rule": { + "object_ownership": "BucketOwnerPreferred" + } + } + } + } +}" +`; + +exports[`renders a Pocket ECS Codepipeline template with the provided artifact bucket prefix 1`] = ` +"{ + "data": { + "aws_iam_policy_document": { + "test-codepipeline_codepipeline-assume-role-policy_AB972C21": { + "statement": [ + { + "actions": [ + "sts:AssumeRole" + ], + "effect": "Allow", + "principals": [ + { + "identifiers": [ + "codepipeline.amazonaws.com" + ], + "type": "Service" + } + ] + } + ] + }, + "test-codepipeline_codepipeline-role-policy-document_DD4EB3DB": { + "statement": [ + { + "actions": [ + "codestar-connections:UseConnection" + ], + "effect": "Allow", + "resources": [ + "arn:codestart-connection:*" + ] + }, + { + "actions": [ + "codebuild:BatchGetBuilds", + "codebuild:StartBuild", + "codebuild:BatchGetBuildBatches", + "codebuild:StartBuildBatch" + ], + "effect": "Allow", + "resources": [ + "arn:aws:codebuild:*:*:project/Test-Env*" + ] + }, + { + "actions": [ + "codedeploy:CreateDeployment", + "codedeploy:GetApplication", + "codedeploy:GetApplicationRevision", + "codedeploy:GetDeployment", + "codedeploy:RegisterApplicationRevision", + "codedeploy:GetDeploymentConfig" + ], + "effect": "Allow", + "resources": [ + "arn:aws:codedeploy:*:*:application:Test-Env-ECS", + "arn:aws:codedeploy:*:*:deploymentgroup:Test-Env-ECS/Test-Env-ECS", + "arn:aws:codedeploy:*:*:deploymentconfig:*" + ] + }, + { + "actions": [ + "s3:GetObject", + "s3:GetObjectVersion", + "s3:GetBucketVersioning", + "s3:PutObjectAcl", + "s3:PutObject" + ], + "effect": "Allow", + "resources": [ + "\${aws_s3_bucket.test-codepipeline_codepipeline-bucket_D4671464.arn}", + "\${aws_s3_bucket.test-codepipeline_codepipeline-bucket_D4671464.arn}/*" + ] + }, + { + "actions": [ + "iam:PassRole" + ], + "condition": [ + { + "test": "StringEqualsIfExists", + "values": [ + "ecs-tasks.amazonaws.com" + ], + "variable": "iam:PassedToService" + } + ], + "effect": "Allow", + "resources": [ + "*" + ] + }, + { + "actions": [ + "ecs:RegisterTaskDefinition" + ], + "effect": "Allow", + "resources": [ + "*" + ] + } + ] + } + }, + "aws_kms_alias": { + "test-codepipeline_kms_s3_alias_6C922DC1": { + "name": "alias/aws/s3" + } + } + }, + "resource": { + "aws_codepipeline": { + "test-codepipeline_367BF1E2": { + "artifact_store": [ + { + "encryption_key": { + "id": "\${data.aws_kms_alias.test-codepipeline_kms_s3_alias_6C922DC1.arn}", + "type": "KMS" + }, + "location": "\${aws_s3_bucket.test-codepipeline_codepipeline-bucket_D4671464.bucket}", + "type": "S3" + } + ], + "name": "Test-Env-CodePipeline", + "role_arn": "\${aws_iam_role.test-codepipeline_codepipeline-role_4D3D2364.arn}", + "stage": [ + { + "action": [ + { + "category": "Source", + "configuration": { + "BranchName": "test", + "ConnectionArn": "arn:codestart-connection:*", + "DetectChanges": "false", + "FullRepositoryId": "string" + }, + "name": "GitHub_Checkout", + "namespace": "SourceVariables", + "output_artifacts": [ + "SourceOutput" + ], + "owner": "AWS", + "provider": "CodeStarSourceConnection", + "version": "1" + } + ], + "name": "Source" + }, + { + "action": [ + { + "category": "Build", + "configuration": { + "EnvironmentVariables": "[{\\"name\\":\\"GIT_BRANCH\\",\\"value\\":\\"#{SourceVariables.BranchName}\\"}]", + "ProjectName": "Test-Env" + }, + "input_artifacts": [ + "SourceOutput" + ], + "name": "Deploy_CDK", + "output_artifacts": [ + "CodeBuildOutput" + ], + "owner": "AWS", + "provider": "CodeBuild", + "run_order": 1, + "version": "1" + }, + { + "category": "Deploy", + "configuration": { + "AppSpecTemplateArtifact": "CodeBuildOutput", + "AppSpecTemplatePath": "appspec.json", + "ApplicationName": "Test-Env-ECS", + "DeploymentGroupName": "Test-Env-ECS", + "TaskDefinitionTemplateArtifact": "CodeBuildOutput", + "TaskDefinitionTemplatePath": "taskdef.json" + }, + "input_artifacts": [ + "CodeBuildOutput" + ], + "name": "Deploy_ECS", + "owner": "AWS", + "provider": "CodeDeployToECS", + "run_order": 2, + "version": "1" + } + ], + "name": "Deploy" + } + ] + } + }, + "aws_iam_role": { + "test-codepipeline_codepipeline-role_4D3D2364": { + "assume_role_policy": "\${data.aws_iam_policy_document.test-codepipeline_codepipeline-assume-role-policy_AB972C21.json}", + "name": "Test-Env-CodePipelineRole" + } + }, + "aws_iam_role_policy": { + "test-codepipeline_codepipeline-role-policy_76FC5FBB": { + "name": "Test-Env-CodePipeline-Role-Policy", + "policy": "\${data.aws_iam_policy_document.test-codepipeline_codepipeline-role-policy-document_DD4EB3DB.json}", + "role": "\${aws_iam_role.test-codepipeline_codepipeline-role_4D3D2364.id}" + } + }, + "aws_s3_bucket": { + "test-codepipeline_codepipeline-bucket_D4671464": { + "bucket": "my-codepipeline-e7e6d80d49c8787d2f7a7aca123082dd", + "force_destroy": true + } + }, + "aws_s3_bucket_acl": { + "test-codepipeline_code-bucket-acl_52349EA5": { + "acl": "private", + "bucket": "\${aws_s3_bucket.test-codepipeline_codepipeline-bucket_D4671464.id}", + "depends_on": [ + "aws_s3_bucket_ownership_controls.test-codepipeline_code-bucket-ownership-controls_737E4D7F" + ] + } + }, + "aws_s3_bucket_ownership_controls": { + "test-codepipeline_code-bucket-ownership-controls_737E4D7F": { + "bucket": "\${aws_s3_bucket.test-codepipeline_codepipeline-bucket_D4671464.id}", + "rule": { + "object_ownership": "BucketOwnerPreferred" + } + } + } + } +}" +`; + +exports[`renders a Pocket ECS Codepipeline template with the provided code build project name 1`] = ` +"{ + "data": { + "aws_iam_policy_document": { + "test-codepipeline_codepipeline-assume-role-policy_AB972C21": { + "statement": [ + { + "actions": [ + "sts:AssumeRole" + ], + "effect": "Allow", + "principals": [ + { + "identifiers": [ + "codepipeline.amazonaws.com" + ], + "type": "Service" + } + ] + } + ] + }, + "test-codepipeline_codepipeline-role-policy-document_DD4EB3DB": { + "statement": [ + { + "actions": [ + "codestar-connections:UseConnection" + ], + "effect": "Allow", + "resources": [ + "arn:codestart-connection:*" + ] + }, + { + "actions": [ + "codebuild:BatchGetBuilds", + "codebuild:StartBuild", + "codebuild:BatchGetBuildBatches", + "codebuild:StartBuildBatch" + ], + "effect": "Allow", + "resources": [ + "arn:aws:codebuild:*:*:project/TestBuildName*" + ] + }, + { + "actions": [ + "codedeploy:CreateDeployment", + "codedeploy:GetApplication", + "codedeploy:GetApplicationRevision", + "codedeploy:GetDeployment", + "codedeploy:RegisterApplicationRevision", + "codedeploy:GetDeploymentConfig" + ], + "effect": "Allow", + "resources": [ + "arn:aws:codedeploy:*:*:application:Test-Env-ECS", + "arn:aws:codedeploy:*:*:deploymentgroup:Test-Env-ECS/Test-Env-ECS", + "arn:aws:codedeploy:*:*:deploymentconfig:*" + ] + }, + { + "actions": [ + "s3:GetObject", + "s3:GetObjectVersion", + "s3:GetBucketVersioning", + "s3:PutObjectAcl", + "s3:PutObject" + ], + "effect": "Allow", + "resources": [ + "\${aws_s3_bucket.test-codepipeline_codepipeline-bucket_D4671464.arn}", + "\${aws_s3_bucket.test-codepipeline_codepipeline-bucket_D4671464.arn}/*" + ] + }, + { + "actions": [ + "iam:PassRole" + ], + "condition": [ + { + "test": "StringEqualsIfExists", + "values": [ + "ecs-tasks.amazonaws.com" + ], + "variable": "iam:PassedToService" + } + ], + "effect": "Allow", + "resources": [ + "*" + ] + }, + { + "actions": [ + "ecs:RegisterTaskDefinition" + ], + "effect": "Allow", + "resources": [ + "*" + ] + } + ] + } + }, + "aws_kms_alias": { + "test-codepipeline_kms_s3_alias_6C922DC1": { + "name": "alias/aws/s3" + } + } + }, + "resource": { + "aws_codepipeline": { + "test-codepipeline_367BF1E2": { + "artifact_store": [ + { + "encryption_key": { + "id": "\${data.aws_kms_alias.test-codepipeline_kms_s3_alias_6C922DC1.arn}", + "type": "KMS" + }, + "location": "\${aws_s3_bucket.test-codepipeline_codepipeline-bucket_D4671464.bucket}", + "type": "S3" + } + ], + "name": "Test-Env-CodePipeline", + "role_arn": "\${aws_iam_role.test-codepipeline_codepipeline-role_4D3D2364.arn}", + "stage": [ + { + "action": [ + { + "category": "Source", + "configuration": { + "BranchName": "test", + "ConnectionArn": "arn:codestart-connection:*", + "DetectChanges": "false", + "FullRepositoryId": "string" + }, + "name": "GitHub_Checkout", + "namespace": "SourceVariables", + "output_artifacts": [ + "SourceOutput" + ], + "owner": "AWS", + "provider": "CodeStarSourceConnection", + "version": "1" + } + ], + "name": "Source" + }, + { + "action": [ + { + "category": "Build", + "configuration": { + "EnvironmentVariables": "[{\\"name\\":\\"GIT_BRANCH\\",\\"value\\":\\"#{SourceVariables.BranchName}\\"}]", + "ProjectName": "TestBuildName" + }, + "input_artifacts": [ + "SourceOutput" + ], + "name": "Deploy_CDK", + "output_artifacts": [ + "CodeBuildOutput" + ], + "owner": "AWS", + "provider": "CodeBuild", + "run_order": 1, + "version": "1" + }, + { + "category": "Deploy", + "configuration": { + "AppSpecTemplateArtifact": "CodeBuildOutput", + "AppSpecTemplatePath": "appspec.json", + "ApplicationName": "Test-Env-ECS", + "DeploymentGroupName": "Test-Env-ECS", + "TaskDefinitionTemplateArtifact": "CodeBuildOutput", + "TaskDefinitionTemplatePath": "taskdef.json" + }, + "input_artifacts": [ + "CodeBuildOutput" + ], + "name": "Deploy_ECS", + "owner": "AWS", + "provider": "CodeDeployToECS", + "run_order": 2, + "version": "1" + } + ], + "name": "Deploy" + } + ] + } + }, + "aws_iam_role": { + "test-codepipeline_codepipeline-role_4D3D2364": { + "assume_role_policy": "\${data.aws_iam_policy_document.test-codepipeline_codepipeline-assume-role-policy_AB972C21.json}", + "name": "Test-Env-CodePipelineRole" + } + }, + "aws_iam_role_policy": { + "test-codepipeline_codepipeline-role-policy_76FC5FBB": { + "name": "Test-Env-CodePipeline-Role-Policy", + "policy": "\${data.aws_iam_policy_document.test-codepipeline_codepipeline-role-policy-document_DD4EB3DB.json}", + "role": "\${aws_iam_role.test-codepipeline_codepipeline-role_4D3D2364.id}" + } + }, + "aws_s3_bucket": { + "test-codepipeline_codepipeline-bucket_D4671464": { + "bucket": "pocket-codepipeline-e7e6d80d49c8787d2f7a7aca123082dd", + "force_destroy": true + } + }, + "aws_s3_bucket_acl": { + "test-codepipeline_code-bucket-acl_52349EA5": { + "acl": "private", + "bucket": "\${aws_s3_bucket.test-codepipeline_codepipeline-bucket_D4671464.id}", + "depends_on": [ + "aws_s3_bucket_ownership_controls.test-codepipeline_code-bucket-ownership-controls_737E4D7F" + ] + } + }, + "aws_s3_bucket_ownership_controls": { + "test-codepipeline_code-bucket-ownership-controls_737E4D7F": { + "bucket": "\${aws_s3_bucket.test-codepipeline_codepipeline-bucket_D4671464.id}", + "rule": { + "object_ownership": "BucketOwnerPreferred" + } + } + } + } +}" +`; + +exports[`renders a Pocket ECS Codepipeline template with the provided taskdef path 1`] = ` +"{ + "data": { + "aws_iam_policy_document": { + "test-codepipeline_codepipeline-assume-role-policy_AB972C21": { + "statement": [ + { + "actions": [ + "sts:AssumeRole" + ], + "effect": "Allow", + "principals": [ + { + "identifiers": [ + "codepipeline.amazonaws.com" + ], + "type": "Service" + } + ] + } + ] + }, + "test-codepipeline_codepipeline-role-policy-document_DD4EB3DB": { + "statement": [ + { + "actions": [ + "codestar-connections:UseConnection" + ], + "effect": "Allow", + "resources": [ + "arn:codestart-connection:*" + ] + }, + { + "actions": [ + "codebuild:BatchGetBuilds", + "codebuild:StartBuild", + "codebuild:BatchGetBuildBatches", + "codebuild:StartBuildBatch" + ], + "effect": "Allow", + "resources": [ + "arn:aws:codebuild:*:*:project/Test-Env*" + ] + }, + { + "actions": [ + "codedeploy:CreateDeployment", + "codedeploy:GetApplication", + "codedeploy:GetApplicationRevision", + "codedeploy:GetDeployment", + "codedeploy:RegisterApplicationRevision", + "codedeploy:GetDeploymentConfig" + ], + "effect": "Allow", + "resources": [ + "arn:aws:codedeploy:*:*:application:Test-Env-ECS", + "arn:aws:codedeploy:*:*:deploymentgroup:Test-Env-ECS/Test-Env-ECS", + "arn:aws:codedeploy:*:*:deploymentconfig:*" + ] + }, + { + "actions": [ + "s3:GetObject", + "s3:GetObjectVersion", + "s3:GetBucketVersioning", + "s3:PutObjectAcl", + "s3:PutObject" + ], + "effect": "Allow", + "resources": [ + "\${aws_s3_bucket.test-codepipeline_codepipeline-bucket_D4671464.arn}", + "\${aws_s3_bucket.test-codepipeline_codepipeline-bucket_D4671464.arn}/*" + ] + }, + { + "actions": [ + "iam:PassRole" + ], + "condition": [ + { + "test": "StringEqualsIfExists", + "values": [ + "ecs-tasks.amazonaws.com" + ], + "variable": "iam:PassedToService" + } + ], + "effect": "Allow", + "resources": [ + "*" + ] + }, + { + "actions": [ + "ecs:RegisterTaskDefinition" + ], + "effect": "Allow", + "resources": [ + "*" + ] + } + ] + } + }, + "aws_kms_alias": { + "test-codepipeline_kms_s3_alias_6C922DC1": { + "name": "alias/aws/s3" + } + } + }, + "resource": { + "aws_codepipeline": { + "test-codepipeline_367BF1E2": { + "artifact_store": [ + { + "encryption_key": { + "id": "\${data.aws_kms_alias.test-codepipeline_kms_s3_alias_6C922DC1.arn}", + "type": "KMS" + }, + "location": "\${aws_s3_bucket.test-codepipeline_codepipeline-bucket_D4671464.bucket}", + "type": "S3" + } + ], + "name": "Test-Env-CodePipeline", + "role_arn": "\${aws_iam_role.test-codepipeline_codepipeline-role_4D3D2364.arn}", + "stage": [ + { + "action": [ + { + "category": "Source", + "configuration": { + "BranchName": "test", + "ConnectionArn": "arn:codestart-connection:*", + "DetectChanges": "false", + "FullRepositoryId": "string" + }, + "name": "GitHub_Checkout", + "namespace": "SourceVariables", + "output_artifacts": [ + "SourceOutput" + ], + "owner": "AWS", + "provider": "CodeStarSourceConnection", + "version": "1" + } + ], + "name": "Source" + }, + { + "action": [ + { + "category": "Build", + "configuration": { + "EnvironmentVariables": "[{\\"name\\":\\"GIT_BRANCH\\",\\"value\\":\\"#{SourceVariables.BranchName}\\"}]", + "ProjectName": "Test-Env" + }, + "input_artifacts": [ + "SourceOutput" + ], + "name": "Deploy_CDK", + "output_artifacts": [ + "CodeBuildOutput" + ], + "owner": "AWS", + "provider": "CodeBuild", + "run_order": 1, + "version": "1" + }, + { + "category": "Deploy", + "configuration": { + "AppSpecTemplateArtifact": "CodeBuildOutput", + "AppSpecTemplatePath": "appspec.json", + "ApplicationName": "Test-Env-ECS", + "DeploymentGroupName": "Test-Env-ECS", + "TaskDefinitionTemplateArtifact": "CodeBuildOutput", + "TaskDefinitionTemplatePath": "testtaskdef.json" + }, + "input_artifacts": [ + "CodeBuildOutput" + ], + "name": "Deploy_ECS", + "owner": "AWS", + "provider": "CodeDeployToECS", + "run_order": 2, + "version": "1" + } + ], + "name": "Deploy" + } + ] + } + }, + "aws_iam_role": { + "test-codepipeline_codepipeline-role_4D3D2364": { + "assume_role_policy": "\${data.aws_iam_policy_document.test-codepipeline_codepipeline-assume-role-policy_AB972C21.json}", + "name": "Test-Env-CodePipelineRole" + } + }, + "aws_iam_role_policy": { + "test-codepipeline_codepipeline-role-policy_76FC5FBB": { + "name": "Test-Env-CodePipeline-Role-Policy", + "policy": "\${data.aws_iam_policy_document.test-codepipeline_codepipeline-role-policy-document_DD4EB3DB.json}", + "role": "\${aws_iam_role.test-codepipeline_codepipeline-role_4D3D2364.id}" + } + }, + "aws_s3_bucket": { + "test-codepipeline_codepipeline-bucket_D4671464": { + "bucket": "pocket-codepipeline-e7e6d80d49c8787d2f7a7aca123082dd", + "force_destroy": true + } + }, + "aws_s3_bucket_acl": { + "test-codepipeline_code-bucket-acl_52349EA5": { + "acl": "private", + "bucket": "\${aws_s3_bucket.test-codepipeline_codepipeline-bucket_D4671464.id}", + "depends_on": [ + "aws_s3_bucket_ownership_controls.test-codepipeline_code-bucket-ownership-controls_737E4D7F" + ] + } + }, + "aws_s3_bucket_ownership_controls": { + "test-codepipeline_code-bucket-ownership-controls_737E4D7F": { + "bucket": "\${aws_s3_bucket.test-codepipeline_codepipeline-bucket_D4671464.id}", + "rule": { + "object_ownership": "BucketOwnerPreferred" + } + } + } + } +}" +`; diff --git a/packages/terraform-modules/src/pocket/__snapshots__/PocketEventBridgeRuleWithMultipleTargets.spec.ts.snap b/packages/terraform-modules/src/pocket/__snapshots__/PocketEventBridgeRuleWithMultipleTargets.spec.ts.snap new file mode 100644 index 00000000..12143087 --- /dev/null +++ b/packages/terraform-modules/src/pocket/__snapshots__/PocketEventBridgeRuleWithMultipleTargets.spec.ts.snap @@ -0,0 +1,269 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`PocketEventBridgeRuleWithMultipleTargets renders an event bridge and multiple targets 1`] = ` +"{ + "data": { + "archive_file": { + "test-lambda_lambda-default-file_7A7DBB68": { + "output_path": "index.py.zip", + "source": [ + { + "content": "import json\\ndef handler(event, context):\\n\\t print(event)\\n\\t return {'statusCode': 200, 'headers': {'dance': 'party'}, 'body': json.dumps({'electric': 'boogaloo'}), 'isBase64Encoded': False}", + "filename": "index.py" + } + ], + "type": "zip" + } + }, + "aws_iam_policy_document": { + "test-lambda_assume-policy-document_D538087D": { + "statement": [ + { + "actions": [ + "sts:AssumeRole" + ], + "effect": "Allow", + "principals": [ + { + "identifiers": [ + "lambda.amazonaws.com", + "edgelambda.amazonaws.com" + ], + "type": "Service" + } + ] + } + ], + "version": "2012-10-17" + }, + "test-lambda_execution-policy-document_D94D17B4": { + "statement": [ + { + "actions": [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents", + "logs:DescribeLogStreams" + ], + "effect": "Allow", + "resources": [ + "arn:aws:logs:*:*:*" + ] + } + ], + "version": "2012-10-17" + } + } + }, + "resource": { + "aws_cloudwatch_event_rule": { + "test-event-bridge-for-multiple-targets-1_event-bridge-rule_1CCF9D7B": { + "description": "Test description", + "event_bus_name": "default", + "event_pattern": "{\\"source\\":[\\"aws.states\\"],\\"detail-type\\":[\\"Step Functions Execution Status Change\\"]}", + "lifecycle": { + }, + "name": "test-event-bridge-rule-multiple-targets-Rule" + } + }, + "aws_cloudwatch_event_target": { + "test-event-bridge-for-multiple-targets-1_event-bridge-rule_event-bridge-target-test-lambda-id_DE65E71F": { + "arn": "lambda.arn", + "dead_letter_config": { + }, + "depends_on": [ + "aws_lambda_alias.test-lambda_alias_CCFDFCE9", + "aws_cloudwatch_event_rule.test-event-bridge-for-multiple-targets-1_event-bridge-rule_1CCF9D7B" + ], + "event_bus_name": "default", + "rule": "\${aws_cloudwatch_event_rule.test-event-bridge-for-multiple-targets-1_event-bridge-rule_1CCF9D7B.name}", + "target_id": "test-lambda-id" + }, + "test-event-bridge-for-multiple-targets-1_event-bridge-rule_event-bridge-target-test-sqs-id_2DE721FF": { + "arn": "\${aws_sqs_queue.test-queue.arn}", + "dead_letter_config": { + }, + "depends_on": [ + "aws_sqs_queue.test-queue", + "aws_cloudwatch_event_rule.test-event-bridge-for-multiple-targets-1_event-bridge-rule_1CCF9D7B" + ], + "event_bus_name": "default", + "rule": "\${aws_cloudwatch_event_rule.test-event-bridge-for-multiple-targets-1_event-bridge-rule_1CCF9D7B.name}", + "target_id": "test-sqs-id" + } + }, + "aws_cloudwatch_log_group": { + "test-lambda_log-group_ACC182B5": { + "depends_on": [ + "aws_lambda_function.test-lambda_8915D118" + ], + "name": "/aws/lambda/\${aws_lambda_function.test-lambda_8915D118.function_name}", + "retention_in_days": 14 + } + }, + "aws_iam_policy": { + "test-lambda_execution-policy_19C785A8": { + "name": "test-lambda-ExecutionRolePolicy", + "policy": "\${data.aws_iam_policy_document.test-lambda_execution-policy-document_D94D17B4.json}" + } + }, + "aws_iam_role": { + "test-lambda_execution-role_9D4EE856": { + "assume_role_policy": "\${data.aws_iam_policy_document.test-lambda_assume-policy-document_D538087D.json}", + "name": "test-lambda-ExecutionRole" + } + }, + "aws_iam_role_policy_attachment": { + "test-lambda_execution-role-policy-attachment_8796505D": { + "depends_on": [ + "aws_iam_role.test-lambda_execution-role_9D4EE856", + "aws_iam_policy.test-lambda_execution-policy_19C785A8" + ], + "policy_arn": "\${aws_iam_policy.test-lambda_execution-policy_19C785A8.arn}", + "role": "\${aws_iam_role.test-lambda_execution-role_9D4EE856.name}" + } + }, + "aws_lambda_alias": { + "test-lambda_alias_CCFDFCE9": { + "depends_on": [ + "aws_lambda_function.test-lambda_8915D118" + ], + "function_name": "\${aws_lambda_function.test-lambda_8915D118.function_name}", + "function_version": "\${element(split(\\":\\", aws_lambda_function.test-lambda_8915D118.qualified_arn), 7)}", + "lifecycle": { + "ignore_changes": [ + "function_version" + ] + }, + "name": "DEPLOYED" + } + }, + "aws_lambda_function": { + "test-lambda_8915D118": { + "filename": "\${data.archive_file.test-lambda_lambda-default-file_7A7DBB68.output_path}", + "function_name": "test-lambda-Function", + "handler": "index.handler", + "lifecycle": { + "ignore_changes": [ + "filename", + "source_code_hash" + ] + }, + "memory_size": 128, + "publish": true, + "reserved_concurrent_executions": -1, + "role": "\${aws_iam_role.test-lambda_execution-role_9D4EE856.arn}", + "runtime": "python3.8", + "source_code_hash": "\${data.archive_file.test-lambda_lambda-default-file_7A7DBB68.output_base64sha256}", + "timeout": 5 + } + }, + "aws_s3_bucket": { + "test-lambda_code-bucket_177F316E": { + "bucket": "pocket-test-lambda", + "force_destroy": true + } + }, + "aws_s3_bucket_acl": { + "test-lambda_code-bucket-acl_C73BD0DB": { + "acl": "private", + "bucket": "\${aws_s3_bucket.test-lambda_code-bucket_177F316E.id}", + "depends_on": [ + "aws_s3_bucket_ownership_controls.test-lambda_code-bucket-ownership-controls_6A1865C3" + ] + } + }, + "aws_s3_bucket_ownership_controls": { + "test-lambda_code-bucket-ownership-controls_6A1865C3": { + "bucket": "\${aws_s3_bucket.test-lambda_code-bucket_177F316E.id}", + "rule": { + "object_ownership": "BucketOwnerPreferred" + } + } + }, + "aws_s3_bucket_public_access_block": { + "test-lambda_code-bucket-public-access-block_1E7CE5AE": { + "block_public_acls": true, + "block_public_policy": true, + "bucket": "\${aws_s3_bucket.test-lambda_code-bucket_177F316E.id}" + } + }, + "aws_sqs_queue": { + "test-queue": { + "name": "Test-SQS-Queue" + } + } + } +}" +`; + +exports[`PocketEventBridgeRuleWithMultipleTargets renders an event bridge and pre-existing targets 1`] = ` +"{ + "resource": { + "aws_cloudwatch_event_rule": { + "test-event-bridge-for-multiple-targets-1_event-bridge-rule_1CCF9D7B": { + "description": "Test description", + "event_bus_name": "default", + "event_pattern": "{\\"source\\":[\\"aws.states\\"],\\"detail-type\\":[\\"Step Functions Execution Status Change\\"]}", + "lifecycle": { + }, + "name": "test-event-bridge-rule-multiple-targets-Rule" + } + }, + "aws_cloudwatch_event_target": { + "test-event-bridge-for-multiple-targets-1_event-bridge-rule_event-bridge-target-test-lambda-id_DE65E71F": { + "arn": "lambda.arn", + "dead_letter_config": { + }, + "event_bus_name": "default", + "rule": "\${aws_cloudwatch_event_rule.test-event-bridge-for-multiple-targets-1_event-bridge-rule_1CCF9D7B.name}", + "target_id": "test-lambda-id" + }, + "test-event-bridge-for-multiple-targets-1_event-bridge-rule_event-bridge-target-test-sqs-id_2DE721FF": { + "arn": "testSqs.arn", + "dead_letter_config": { + }, + "event_bus_name": "default", + "rule": "\${aws_cloudwatch_event_rule.test-event-bridge-for-multiple-targets-1_event-bridge-rule_1CCF9D7B.name}", + "target_id": "test-sqs-id" + } + } + } +}" +`; + +exports[`PocketEventBridgeRuleWithMultipleTargets renders an event bridge rule with prevent destroy flag 1`] = ` +"{ + "resource": { + "aws_cloudwatch_event_rule": { + "test-event-bridge-for-multiple-targets-1_event-bridge-rule_1CCF9D7B": { + "description": "Test description", + "event_bus_name": "default", + "event_pattern": "{\\"source\\":[\\"aws.states\\"],\\"detail-type\\":[\\"Step Functions Execution Status Change\\"]}", + "lifecycle": { + "prevent_destroy": true + }, + "name": "test-event-bridge-rule-multiple-targets-Rule" + } + }, + "aws_cloudwatch_event_target": { + "test-event-bridge-for-multiple-targets-1_event-bridge-rule_event-bridge-target-test-lambda-id_DE65E71F": { + "arn": "lambda.arn", + "dead_letter_config": { + }, + "event_bus_name": "default", + "rule": "\${aws_cloudwatch_event_rule.test-event-bridge-for-multiple-targets-1_event-bridge-rule_1CCF9D7B.name}", + "target_id": "test-lambda-id" + }, + "test-event-bridge-for-multiple-targets-1_event-bridge-rule_event-bridge-target-test-sqs-id_2DE721FF": { + "arn": "testSqs.arn", + "dead_letter_config": { + }, + "event_bus_name": "default", + "rule": "\${aws_cloudwatch_event_rule.test-event-bridge-for-multiple-targets-1_event-bridge-rule_1CCF9D7B.name}", + "target_id": "test-sqs-id" + } + } + } +}" +`; diff --git a/packages/terraform-modules/src/pocket/__snapshots__/PocketEventBridgeWithLambdaTarget.spec.ts.snap b/packages/terraform-modules/src/pocket/__snapshots__/PocketEventBridgeWithLambdaTarget.spec.ts.snap new file mode 100644 index 00000000..59b615d7 --- /dev/null +++ b/packages/terraform-modules/src/pocket/__snapshots__/PocketEventBridgeWithLambdaTarget.spec.ts.snap @@ -0,0 +1,386 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`renders an event bridge and lambda target with event bus name 1`] = ` +"{ + "data": { + "archive_file": { + "test-event-bridge-lambda_lambda-default-file_57164BB7": { + "output_path": "index.py.zip", + "source": [ + { + "content": "import json\\ndef handler(event, context):\\n\\t print(event)\\n\\t return {'statusCode': 200, 'headers': {'dance': 'party'}, 'body': json.dumps({'electric': 'boogaloo'}), 'isBase64Encoded': False}", + "filename": "index.py" + } + ], + "type": "zip" + } + }, + "aws_iam_policy_document": { + "test-event-bridge-lambda_assume-policy-document_59E97C0B": { + "statement": [ + { + "actions": [ + "sts:AssumeRole" + ], + "effect": "Allow", + "principals": [ + { + "identifiers": [ + "lambda.amazonaws.com", + "edgelambda.amazonaws.com" + ], + "type": "Service" + } + ] + } + ], + "version": "2012-10-17" + }, + "test-event-bridge-lambda_execution-policy-document_0A3C0E28": { + "statement": [ + { + "actions": [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents", + "logs:DescribeLogStreams" + ], + "effect": "Allow", + "resources": [ + "arn:aws:logs:*:*:*" + ] + } + ], + "version": "2012-10-17" + } + } + }, + "resource": { + "aws_cloudwatch_event_rule": { + "test-event-bridge-lambda_event-bridge-rule_529FF7C2": { + "event_bus_name": "test-bus", + "event_pattern": "{\\"source\\":[\\"aws.states\\"],\\"detail-type\\":[\\"Step Functions Execution Status Change\\"]}", + "lifecycle": { + }, + "name": "test-event-bridge-lambda-Rule" + } + }, + "aws_cloudwatch_event_target": { + "test-event-bridge-lambda_event-bridge-rule_event-bridge-target-lambda_30D0509D": { + "arn": "\${aws_lambda_alias.test-event-bridge-lambda_alias_2EC274FC.arn}", + "dead_letter_config": { + }, + "depends_on": [ + "aws_lambda_alias.test-event-bridge-lambda_alias_2EC274FC", + "aws_cloudwatch_event_rule.test-event-bridge-lambda_event-bridge-rule_529FF7C2" + ], + "event_bus_name": "test-bus", + "rule": "\${aws_cloudwatch_event_rule.test-event-bridge-lambda_event-bridge-rule_529FF7C2.name}", + "target_id": "lambda" + } + }, + "aws_cloudwatch_log_group": { + "test-event-bridge-lambda_log-group_CF4CF8D4": { + "depends_on": [ + "aws_lambda_function.test-event-bridge-lambda_65C2E1CE" + ], + "name": "/aws/lambda/\${aws_lambda_function.test-event-bridge-lambda_65C2E1CE.function_name}", + "retention_in_days": 14 + } + }, + "aws_iam_policy": { + "test-event-bridge-lambda_execution-policy_F6422B9B": { + "name": "test-event-bridge-lambda-ExecutionRolePolicy", + "policy": "\${data.aws_iam_policy_document.test-event-bridge-lambda_execution-policy-document_0A3C0E28.json}" + } + }, + "aws_iam_role": { + "test-event-bridge-lambda_execution-role_7936A26A": { + "assume_role_policy": "\${data.aws_iam_policy_document.test-event-bridge-lambda_assume-policy-document_59E97C0B.json}", + "name": "test-event-bridge-lambda-ExecutionRole" + } + }, + "aws_iam_role_policy_attachment": { + "test-event-bridge-lambda_execution-role-policy-attachment_2614BB3C": { + "depends_on": [ + "aws_iam_role.test-event-bridge-lambda_execution-role_7936A26A", + "aws_iam_policy.test-event-bridge-lambda_execution-policy_F6422B9B" + ], + "policy_arn": "\${aws_iam_policy.test-event-bridge-lambda_execution-policy_F6422B9B.arn}", + "role": "\${aws_iam_role.test-event-bridge-lambda_execution-role_7936A26A.name}" + } + }, + "aws_lambda_alias": { + "test-event-bridge-lambda_alias_2EC274FC": { + "depends_on": [ + "aws_lambda_function.test-event-bridge-lambda_65C2E1CE" + ], + "function_name": "\${aws_lambda_function.test-event-bridge-lambda_65C2E1CE.function_name}", + "function_version": "\${element(split(\\":\\", aws_lambda_function.test-event-bridge-lambda_65C2E1CE.qualified_arn), 7)}", + "lifecycle": { + "ignore_changes": [ + "function_version" + ] + }, + "name": "DEPLOYED" + } + }, + "aws_lambda_function": { + "test-event-bridge-lambda_65C2E1CE": { + "filename": "\${data.archive_file.test-event-bridge-lambda_lambda-default-file_57164BB7.output_path}", + "function_name": "test-event-bridge-lambda-Function", + "handler": "index.handler", + "lifecycle": { + "ignore_changes": [ + "filename", + "source_code_hash" + ] + }, + "memory_size": 128, + "publish": true, + "reserved_concurrent_executions": -1, + "role": "\${aws_iam_role.test-event-bridge-lambda_execution-role_7936A26A.arn}", + "runtime": "python3.8", + "source_code_hash": "\${data.archive_file.test-event-bridge-lambda_lambda-default-file_57164BB7.output_base64sha256}", + "timeout": 5 + } + }, + "aws_lambda_permission": { + "test-event-bridge-lambda_lambda-permission_D900B300": { + "action": "lambda:InvokeFunction", + "depends_on": [ + "aws_lambda_alias.test-event-bridge-lambda_alias_2EC274FC", + "aws_cloudwatch_event_rule.test-event-bridge-lambda_event-bridge-rule_529FF7C2" + ], + "function_name": "\${aws_lambda_alias.test-event-bridge-lambda_alias_2EC274FC.function_name}", + "principal": "events.amazonaws.com", + "qualifier": "\${aws_lambda_alias.test-event-bridge-lambda_alias_2EC274FC.name}", + "source_arn": "\${aws_cloudwatch_event_rule.test-event-bridge-lambda_event-bridge-rule_529FF7C2.arn}" + } + }, + "aws_s3_bucket": { + "test-event-bridge-lambda_code-bucket_3703E73B": { + "bucket": "pocket-test-event-bridge-lambda", + "force_destroy": true + } + }, + "aws_s3_bucket_acl": { + "test-event-bridge-lambda_code-bucket-acl_052113EC": { + "acl": "private", + "bucket": "\${aws_s3_bucket.test-event-bridge-lambda_code-bucket_3703E73B.id}", + "depends_on": [ + "aws_s3_bucket_ownership_controls.test-event-bridge-lambda_code-bucket-ownership-controls_88B8181A" + ] + } + }, + "aws_s3_bucket_ownership_controls": { + "test-event-bridge-lambda_code-bucket-ownership-controls_88B8181A": { + "bucket": "\${aws_s3_bucket.test-event-bridge-lambda_code-bucket_3703E73B.id}", + "rule": { + "object_ownership": "BucketOwnerPreferred" + } + } + }, + "aws_s3_bucket_public_access_block": { + "test-event-bridge-lambda_code-bucket-public-access-block_A1A4EF9D": { + "block_public_acls": true, + "block_public_policy": true, + "bucket": "\${aws_s3_bucket.test-event-bridge-lambda_code-bucket_3703E73B.id}" + } + } + } +}" +`; + +exports[`renders an event bridge and lambda target with rule description 1`] = ` +"{ + "data": { + "archive_file": { + "test-event-bridge-lambda_lambda-default-file_57164BB7": { + "output_path": "index.py.zip", + "source": [ + { + "content": "import json\\ndef handler(event, context):\\n\\t print(event)\\n\\t return {'statusCode': 200, 'headers': {'dance': 'party'}, 'body': json.dumps({'electric': 'boogaloo'}), 'isBase64Encoded': False}", + "filename": "index.py" + } + ], + "type": "zip" + } + }, + "aws_iam_policy_document": { + "test-event-bridge-lambda_assume-policy-document_59E97C0B": { + "statement": [ + { + "actions": [ + "sts:AssumeRole" + ], + "effect": "Allow", + "principals": [ + { + "identifiers": [ + "lambda.amazonaws.com", + "edgelambda.amazonaws.com" + ], + "type": "Service" + } + ] + } + ], + "version": "2012-10-17" + }, + "test-event-bridge-lambda_execution-policy-document_0A3C0E28": { + "statement": [ + { + "actions": [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents", + "logs:DescribeLogStreams" + ], + "effect": "Allow", + "resources": [ + "arn:aws:logs:*:*:*" + ] + } + ], + "version": "2012-10-17" + } + } + }, + "resource": { + "aws_cloudwatch_event_rule": { + "test-event-bridge-lambda_event-bridge-rule_529FF7C2": { + "description": "Test description", + "event_bus_name": "default", + "event_pattern": "{\\"source\\":[\\"aws.states\\"],\\"detail-type\\":[\\"Step Functions Execution Status Change\\"]}", + "lifecycle": { + }, + "name": "test-event-bridge-lambda-Rule" + } + }, + "aws_cloudwatch_event_target": { + "test-event-bridge-lambda_event-bridge-rule_event-bridge-target-lambda_30D0509D": { + "arn": "\${aws_lambda_alias.test-event-bridge-lambda_alias_2EC274FC.arn}", + "dead_letter_config": { + }, + "depends_on": [ + "aws_lambda_alias.test-event-bridge-lambda_alias_2EC274FC", + "aws_cloudwatch_event_rule.test-event-bridge-lambda_event-bridge-rule_529FF7C2" + ], + "event_bus_name": "default", + "rule": "\${aws_cloudwatch_event_rule.test-event-bridge-lambda_event-bridge-rule_529FF7C2.name}", + "target_id": "lambda" + } + }, + "aws_cloudwatch_log_group": { + "test-event-bridge-lambda_log-group_CF4CF8D4": { + "depends_on": [ + "aws_lambda_function.test-event-bridge-lambda_65C2E1CE" + ], + "name": "/aws/lambda/\${aws_lambda_function.test-event-bridge-lambda_65C2E1CE.function_name}", + "retention_in_days": 14 + } + }, + "aws_iam_policy": { + "test-event-bridge-lambda_execution-policy_F6422B9B": { + "name": "test-event-bridge-lambda-ExecutionRolePolicy", + "policy": "\${data.aws_iam_policy_document.test-event-bridge-lambda_execution-policy-document_0A3C0E28.json}" + } + }, + "aws_iam_role": { + "test-event-bridge-lambda_execution-role_7936A26A": { + "assume_role_policy": "\${data.aws_iam_policy_document.test-event-bridge-lambda_assume-policy-document_59E97C0B.json}", + "name": "test-event-bridge-lambda-ExecutionRole" + } + }, + "aws_iam_role_policy_attachment": { + "test-event-bridge-lambda_execution-role-policy-attachment_2614BB3C": { + "depends_on": [ + "aws_iam_role.test-event-bridge-lambda_execution-role_7936A26A", + "aws_iam_policy.test-event-bridge-lambda_execution-policy_F6422B9B" + ], + "policy_arn": "\${aws_iam_policy.test-event-bridge-lambda_execution-policy_F6422B9B.arn}", + "role": "\${aws_iam_role.test-event-bridge-lambda_execution-role_7936A26A.name}" + } + }, + "aws_lambda_alias": { + "test-event-bridge-lambda_alias_2EC274FC": { + "depends_on": [ + "aws_lambda_function.test-event-bridge-lambda_65C2E1CE" + ], + "function_name": "\${aws_lambda_function.test-event-bridge-lambda_65C2E1CE.function_name}", + "function_version": "\${element(split(\\":\\", aws_lambda_function.test-event-bridge-lambda_65C2E1CE.qualified_arn), 7)}", + "lifecycle": { + "ignore_changes": [ + "function_version" + ] + }, + "name": "DEPLOYED" + } + }, + "aws_lambda_function": { + "test-event-bridge-lambda_65C2E1CE": { + "filename": "\${data.archive_file.test-event-bridge-lambda_lambda-default-file_57164BB7.output_path}", + "function_name": "test-event-bridge-lambda-Function", + "handler": "index.handler", + "lifecycle": { + "ignore_changes": [ + "filename", + "source_code_hash" + ] + }, + "memory_size": 128, + "publish": true, + "reserved_concurrent_executions": -1, + "role": "\${aws_iam_role.test-event-bridge-lambda_execution-role_7936A26A.arn}", + "runtime": "python3.8", + "source_code_hash": "\${data.archive_file.test-event-bridge-lambda_lambda-default-file_57164BB7.output_base64sha256}", + "timeout": 5 + } + }, + "aws_lambda_permission": { + "test-event-bridge-lambda_lambda-permission_D900B300": { + "action": "lambda:InvokeFunction", + "depends_on": [ + "aws_lambda_alias.test-event-bridge-lambda_alias_2EC274FC", + "aws_cloudwatch_event_rule.test-event-bridge-lambda_event-bridge-rule_529FF7C2" + ], + "function_name": "\${aws_lambda_alias.test-event-bridge-lambda_alias_2EC274FC.function_name}", + "principal": "events.amazonaws.com", + "qualifier": "\${aws_lambda_alias.test-event-bridge-lambda_alias_2EC274FC.name}", + "source_arn": "\${aws_cloudwatch_event_rule.test-event-bridge-lambda_event-bridge-rule_529FF7C2.arn}" + } + }, + "aws_s3_bucket": { + "test-event-bridge-lambda_code-bucket_3703E73B": { + "bucket": "pocket-test-event-bridge-lambda", + "force_destroy": true + } + }, + "aws_s3_bucket_acl": { + "test-event-bridge-lambda_code-bucket-acl_052113EC": { + "acl": "private", + "bucket": "\${aws_s3_bucket.test-event-bridge-lambda_code-bucket_3703E73B.id}", + "depends_on": [ + "aws_s3_bucket_ownership_controls.test-event-bridge-lambda_code-bucket-ownership-controls_88B8181A" + ] + } + }, + "aws_s3_bucket_ownership_controls": { + "test-event-bridge-lambda_code-bucket-ownership-controls_88B8181A": { + "bucket": "\${aws_s3_bucket.test-event-bridge-lambda_code-bucket_3703E73B.id}", + "rule": { + "object_ownership": "BucketOwnerPreferred" + } + } + }, + "aws_s3_bucket_public_access_block": { + "test-event-bridge-lambda_code-bucket-public-access-block_A1A4EF9D": { + "block_public_acls": true, + "block_public_policy": true, + "bucket": "\${aws_s3_bucket.test-event-bridge-lambda_code-bucket_3703E73B.id}" + } + } + } +}" +`; diff --git a/packages/terraform-modules/src/pocket/__snapshots__/PocketPagerDuty.spec.ts.snap b/packages/terraform-modules/src/pocket/__snapshots__/PocketPagerDuty.spec.ts.snap new file mode 100644 index 00000000..c429c4b5 --- /dev/null +++ b/packages/terraform-modules/src/pocket/__snapshots__/PocketPagerDuty.spec.ts.snap @@ -0,0 +1,568 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`renders a Pocket PagerDuty for critical and non-critical actions 1`] = ` +"{ + "data": { + "pagerduty_vendor": { + "test-pagerduty_cloudwatch_8824B7B3": { + "name": "Cloudwatch" + }, + "test-pagerduty_sentry_896D6C2C": { + "name": "Sentry" + } + } + }, + "resource": { + "aws_sns_topic": { + "test-pagerduty_alarm-critical-topic_5B8FEE8D": { + "name": "Test-Env-Infrastructure-Alarm-Critical", + "tags": { + } + }, + "test-pagerduty_alarm-non-critical-topic_3FD65796": { + "name": "Test-Env-Infrastructure-Alarm-Non-Critical", + "tags": { + } + } + }, + "aws_sns_topic_subscription": { + "test-pagerduty_alarm-critical-subscription_ABC78F4F": { + "confirmation_timeout_in_minutes": 2, + "depends_on": [ + "aws_sns_topic.test-pagerduty_alarm-critical-topic_5B8FEE8D", + "pagerduty_service_integration.test-pagerduty_test-pagerduty_cloudwatch_8824B7B3-critical_29FC10B0" + ], + "endpoint": "https://events.pagerduty.com/integration/\${pagerduty_service_integration.test-pagerduty_test-pagerduty_cloudwatch_8824B7B3-critical_29FC10B0.integration_key}/enqueue", + "endpoint_auto_confirms": true, + "protocol": "https", + "topic_arn": "\${aws_sns_topic.test-pagerduty_alarm-critical-topic_5B8FEE8D.arn}" + }, + "test-pagerduty_alarm-non-critical-subscription_D80993D9": { + "confirmation_timeout_in_minutes": 2, + "depends_on": [ + "aws_sns_topic.test-pagerduty_alarm-non-critical-topic_3FD65796", + "pagerduty_service_integration.test-pagerduty_test-pagerduty_cloudwatch_8824B7B3-non-critical_496C437A" + ], + "endpoint": "https://events.pagerduty.com/integration/\${pagerduty_service_integration.test-pagerduty_test-pagerduty_cloudwatch_8824B7B3-non-critical_496C437A.integration_key}/enqueue", + "endpoint_auto_confirms": true, + "protocol": "https", + "topic_arn": "\${aws_sns_topic.test-pagerduty_alarm-non-critical-topic_3FD65796.arn}" + } + }, + "pagerduty_service": { + "test-pagerduty_pagerduty-critical_3EB714BD": { + "acknowledgement_timeout": "1800", + "alert_creation": "create_incidents", + "auto_resolve_timeout": "14400", + "description": "PagerDuty Critical", + "escalation_policy": "critical-id", + "incident_urgency_rule": { + "type": "constant", + "urgency": "high" + }, + "name": "Test-Env-PagerDuty-Critical" + }, + "test-pagerduty_pagerduty-non-critical_BBC21A2B": { + "acknowledgement_timeout": "1800", + "alert_creation": "create_incidents", + "auto_resolve_timeout": "14400", + "description": "PagerDuty Non-Critical", + "escalation_policy": "non-critical-id", + "incident_urgency_rule": { + "type": "constant", + "urgency": "low" + }, + "name": "Test-Env-PagerDuty-Non-Critical" + } + }, + "pagerduty_service_integration": { + "test-pagerduty_test-pagerduty_cloudwatch_8824B7B3-critical_29FC10B0": { + "depends_on": [ + "pagerduty_service.test-pagerduty_pagerduty-critical_3EB714BD" + ], + "name": "\${data.pagerduty_vendor.test-pagerduty_cloudwatch_8824B7B3.name}", + "service": "\${pagerduty_service.test-pagerduty_pagerduty-critical_3EB714BD.id}", + "vendor": "\${data.pagerduty_vendor.test-pagerduty_cloudwatch_8824B7B3.id}" + }, + "test-pagerduty_test-pagerduty_cloudwatch_8824B7B3-non-critical_496C437A": { + "depends_on": [ + "pagerduty_service.test-pagerduty_pagerduty-non-critical_BBC21A2B" + ], + "name": "\${data.pagerduty_vendor.test-pagerduty_cloudwatch_8824B7B3.name}", + "service": "\${pagerduty_service.test-pagerduty_pagerduty-non-critical_BBC21A2B.id}", + "vendor": "\${data.pagerduty_vendor.test-pagerduty_cloudwatch_8824B7B3.id}" + }, + "test-pagerduty_test-pagerduty_sentry_896D6C2C-critical_B5B900DE": { + "depends_on": [ + "pagerduty_service.test-pagerduty_pagerduty-critical_3EB714BD" + ], + "name": "\${data.pagerduty_vendor.test-pagerduty_sentry_896D6C2C.name}", + "service": "\${pagerduty_service.test-pagerduty_pagerduty-critical_3EB714BD.id}", + "vendor": "\${data.pagerduty_vendor.test-pagerduty_sentry_896D6C2C.id}" + }, + "test-pagerduty_test-pagerduty_sentry_896D6C2C-non-critical_15A9D0BD": { + "depends_on": [ + "pagerduty_service.test-pagerduty_pagerduty-non-critical_BBC21A2B" + ], + "name": "\${data.pagerduty_vendor.test-pagerduty_sentry_896D6C2C.name}", + "service": "\${pagerduty_service.test-pagerduty_pagerduty-non-critical_BBC21A2B.id}", + "vendor": "\${data.pagerduty_vendor.test-pagerduty_sentry_896D6C2C.id}" + } + } + } +}" +`; + +exports[`renders a Pocket PagerDuty with custom acknowledgement timeout 1`] = ` +"{ + "data": { + "pagerduty_vendor": { + "test-pagerduty_cloudwatch_8824B7B3": { + "name": "Cloudwatch" + }, + "test-pagerduty_sentry_896D6C2C": { + "name": "Sentry" + } + } + }, + "resource": { + "aws_sns_topic": { + "test-pagerduty_alarm-critical-topic_5B8FEE8D": { + "name": "Test-Env-Infrastructure-Alarm-Critical", + "tags": { + } + }, + "test-pagerduty_alarm-non-critical-topic_3FD65796": { + "name": "Test-Env-Infrastructure-Alarm-Non-Critical", + "tags": { + } + } + }, + "aws_sns_topic_subscription": { + "test-pagerduty_alarm-critical-subscription_ABC78F4F": { + "confirmation_timeout_in_minutes": 2, + "depends_on": [ + "aws_sns_topic.test-pagerduty_alarm-critical-topic_5B8FEE8D", + "pagerduty_service_integration.test-pagerduty_test-pagerduty_cloudwatch_8824B7B3-critical_29FC10B0" + ], + "endpoint": "https://events.pagerduty.com/integration/\${pagerduty_service_integration.test-pagerduty_test-pagerduty_cloudwatch_8824B7B3-critical_29FC10B0.integration_key}/enqueue", + "endpoint_auto_confirms": true, + "protocol": "https", + "topic_arn": "\${aws_sns_topic.test-pagerduty_alarm-critical-topic_5B8FEE8D.arn}" + }, + "test-pagerduty_alarm-non-critical-subscription_D80993D9": { + "confirmation_timeout_in_minutes": 2, + "depends_on": [ + "aws_sns_topic.test-pagerduty_alarm-non-critical-topic_3FD65796", + "pagerduty_service_integration.test-pagerduty_test-pagerduty_cloudwatch_8824B7B3-non-critical_496C437A" + ], + "endpoint": "https://events.pagerduty.com/integration/\${pagerduty_service_integration.test-pagerduty_test-pagerduty_cloudwatch_8824B7B3-non-critical_496C437A.integration_key}/enqueue", + "endpoint_auto_confirms": true, + "protocol": "https", + "topic_arn": "\${aws_sns_topic.test-pagerduty_alarm-non-critical-topic_3FD65796.arn}" + } + }, + "pagerduty_service": { + "test-pagerduty_pagerduty-critical_3EB714BD": { + "acknowledgement_timeout": "100", + "alert_creation": "create_incidents", + "auto_resolve_timeout": "14400", + "description": "PagerDuty Critical", + "escalation_policy": "critical-id", + "incident_urgency_rule": { + "type": "constant", + "urgency": "high" + }, + "name": "Test-Env-PagerDuty-Critical" + }, + "test-pagerduty_pagerduty-non-critical_BBC21A2B": { + "acknowledgement_timeout": "100", + "alert_creation": "create_incidents", + "auto_resolve_timeout": "14400", + "description": "PagerDuty Non-Critical", + "escalation_policy": "non-critical-id", + "incident_urgency_rule": { + "type": "constant", + "urgency": "low" + }, + "name": "Test-Env-PagerDuty-Non-Critical" + } + }, + "pagerduty_service_integration": { + "test-pagerduty_test-pagerduty_cloudwatch_8824B7B3-critical_29FC10B0": { + "depends_on": [ + "pagerduty_service.test-pagerduty_pagerduty-critical_3EB714BD" + ], + "name": "\${data.pagerduty_vendor.test-pagerduty_cloudwatch_8824B7B3.name}", + "service": "\${pagerduty_service.test-pagerduty_pagerduty-critical_3EB714BD.id}", + "vendor": "\${data.pagerduty_vendor.test-pagerduty_cloudwatch_8824B7B3.id}" + }, + "test-pagerduty_test-pagerduty_cloudwatch_8824B7B3-non-critical_496C437A": { + "depends_on": [ + "pagerduty_service.test-pagerduty_pagerduty-non-critical_BBC21A2B" + ], + "name": "\${data.pagerduty_vendor.test-pagerduty_cloudwatch_8824B7B3.name}", + "service": "\${pagerduty_service.test-pagerduty_pagerduty-non-critical_BBC21A2B.id}", + "vendor": "\${data.pagerduty_vendor.test-pagerduty_cloudwatch_8824B7B3.id}" + }, + "test-pagerduty_test-pagerduty_sentry_896D6C2C-critical_B5B900DE": { + "depends_on": [ + "pagerduty_service.test-pagerduty_pagerduty-critical_3EB714BD" + ], + "name": "\${data.pagerduty_vendor.test-pagerduty_sentry_896D6C2C.name}", + "service": "\${pagerduty_service.test-pagerduty_pagerduty-critical_3EB714BD.id}", + "vendor": "\${data.pagerduty_vendor.test-pagerduty_sentry_896D6C2C.id}" + }, + "test-pagerduty_test-pagerduty_sentry_896D6C2C-non-critical_15A9D0BD": { + "depends_on": [ + "pagerduty_service.test-pagerduty_pagerduty-non-critical_BBC21A2B" + ], + "name": "\${data.pagerduty_vendor.test-pagerduty_sentry_896D6C2C.name}", + "service": "\${pagerduty_service.test-pagerduty_pagerduty-non-critical_BBC21A2B.id}", + "vendor": "\${data.pagerduty_vendor.test-pagerduty_sentry_896D6C2C.id}" + } + } + } +}" +`; + +exports[`renders a Pocket PagerDuty with custom auto resolve timeout 1`] = ` +"{ + "data": { + "pagerduty_vendor": { + "test-pagerduty_cloudwatch_8824B7B3": { + "name": "Cloudwatch" + }, + "test-pagerduty_sentry_896D6C2C": { + "name": "Sentry" + } + } + }, + "resource": { + "aws_sns_topic": { + "test-pagerduty_alarm-critical-topic_5B8FEE8D": { + "name": "Test-Env-Infrastructure-Alarm-Critical", + "tags": { + } + }, + "test-pagerduty_alarm-non-critical-topic_3FD65796": { + "name": "Test-Env-Infrastructure-Alarm-Non-Critical", + "tags": { + } + } + }, + "aws_sns_topic_subscription": { + "test-pagerduty_alarm-critical-subscription_ABC78F4F": { + "confirmation_timeout_in_minutes": 2, + "depends_on": [ + "aws_sns_topic.test-pagerduty_alarm-critical-topic_5B8FEE8D", + "pagerduty_service_integration.test-pagerduty_test-pagerduty_cloudwatch_8824B7B3-critical_29FC10B0" + ], + "endpoint": "https://events.pagerduty.com/integration/\${pagerduty_service_integration.test-pagerduty_test-pagerduty_cloudwatch_8824B7B3-critical_29FC10B0.integration_key}/enqueue", + "endpoint_auto_confirms": true, + "protocol": "https", + "topic_arn": "\${aws_sns_topic.test-pagerduty_alarm-critical-topic_5B8FEE8D.arn}" + }, + "test-pagerduty_alarm-non-critical-subscription_D80993D9": { + "confirmation_timeout_in_minutes": 2, + "depends_on": [ + "aws_sns_topic.test-pagerduty_alarm-non-critical-topic_3FD65796", + "pagerduty_service_integration.test-pagerduty_test-pagerduty_cloudwatch_8824B7B3-non-critical_496C437A" + ], + "endpoint": "https://events.pagerduty.com/integration/\${pagerduty_service_integration.test-pagerduty_test-pagerduty_cloudwatch_8824B7B3-non-critical_496C437A.integration_key}/enqueue", + "endpoint_auto_confirms": true, + "protocol": "https", + "topic_arn": "\${aws_sns_topic.test-pagerduty_alarm-non-critical-topic_3FD65796.arn}" + } + }, + "pagerduty_service": { + "test-pagerduty_pagerduty-critical_3EB714BD": { + "acknowledgement_timeout": "1800", + "alert_creation": "create_incidents", + "auto_resolve_timeout": "4", + "description": "PagerDuty Critical", + "escalation_policy": "critical-id", + "incident_urgency_rule": { + "type": "constant", + "urgency": "high" + }, + "name": "Test-Env-PagerDuty-Critical" + }, + "test-pagerduty_pagerduty-non-critical_BBC21A2B": { + "acknowledgement_timeout": "1800", + "alert_creation": "create_incidents", + "auto_resolve_timeout": "4", + "description": "PagerDuty Non-Critical", + "escalation_policy": "non-critical-id", + "incident_urgency_rule": { + "type": "constant", + "urgency": "low" + }, + "name": "Test-Env-PagerDuty-Non-Critical" + } + }, + "pagerduty_service_integration": { + "test-pagerduty_test-pagerduty_cloudwatch_8824B7B3-critical_29FC10B0": { + "depends_on": [ + "pagerduty_service.test-pagerduty_pagerduty-critical_3EB714BD" + ], + "name": "\${data.pagerduty_vendor.test-pagerduty_cloudwatch_8824B7B3.name}", + "service": "\${pagerduty_service.test-pagerduty_pagerduty-critical_3EB714BD.id}", + "vendor": "\${data.pagerduty_vendor.test-pagerduty_cloudwatch_8824B7B3.id}" + }, + "test-pagerduty_test-pagerduty_cloudwatch_8824B7B3-non-critical_496C437A": { + "depends_on": [ + "pagerduty_service.test-pagerduty_pagerduty-non-critical_BBC21A2B" + ], + "name": "\${data.pagerduty_vendor.test-pagerduty_cloudwatch_8824B7B3.name}", + "service": "\${pagerduty_service.test-pagerduty_pagerduty-non-critical_BBC21A2B.id}", + "vendor": "\${data.pagerduty_vendor.test-pagerduty_cloudwatch_8824B7B3.id}" + }, + "test-pagerduty_test-pagerduty_sentry_896D6C2C-critical_B5B900DE": { + "depends_on": [ + "pagerduty_service.test-pagerduty_pagerduty-critical_3EB714BD" + ], + "name": "\${data.pagerduty_vendor.test-pagerduty_sentry_896D6C2C.name}", + "service": "\${pagerduty_service.test-pagerduty_pagerduty-critical_3EB714BD.id}", + "vendor": "\${data.pagerduty_vendor.test-pagerduty_sentry_896D6C2C.id}" + }, + "test-pagerduty_test-pagerduty_sentry_896D6C2C-non-critical_15A9D0BD": { + "depends_on": [ + "pagerduty_service.test-pagerduty_pagerduty-non-critical_BBC21A2B" + ], + "name": "\${data.pagerduty_vendor.test-pagerduty_sentry_896D6C2C.name}", + "service": "\${pagerduty_service.test-pagerduty_pagerduty-non-critical_BBC21A2B.id}", + "vendor": "\${data.pagerduty_vendor.test-pagerduty_sentry_896D6C2C.id}" + } + } + } +}" +`; + +exports[`renders a Pocket PagerDuty with custom sns topic subscription confirmation timeout in minutes 1`] = ` +"{ + "data": { + "pagerduty_vendor": { + "test-pagerduty_cloudwatch_8824B7B3": { + "name": "Cloudwatch" + }, + "test-pagerduty_sentry_896D6C2C": { + "name": "Sentry" + } + } + }, + "resource": { + "aws_sns_topic": { + "test-pagerduty_alarm-critical-topic_5B8FEE8D": { + "name": "Test-Env-Infrastructure-Alarm-Critical", + "tags": { + } + }, + "test-pagerduty_alarm-non-critical-topic_3FD65796": { + "name": "Test-Env-Infrastructure-Alarm-Non-Critical", + "tags": { + } + } + }, + "aws_sns_topic_subscription": { + "test-pagerduty_alarm-critical-subscription_ABC78F4F": { + "confirmation_timeout_in_minutes": 10, + "depends_on": [ + "aws_sns_topic.test-pagerduty_alarm-critical-topic_5B8FEE8D", + "pagerduty_service_integration.test-pagerduty_test-pagerduty_cloudwatch_8824B7B3-critical_29FC10B0" + ], + "endpoint": "https://events.pagerduty.com/integration/\${pagerduty_service_integration.test-pagerduty_test-pagerduty_cloudwatch_8824B7B3-critical_29FC10B0.integration_key}/enqueue", + "endpoint_auto_confirms": true, + "protocol": "https", + "topic_arn": "\${aws_sns_topic.test-pagerduty_alarm-critical-topic_5B8FEE8D.arn}" + }, + "test-pagerduty_alarm-non-critical-subscription_D80993D9": { + "confirmation_timeout_in_minutes": 10, + "depends_on": [ + "aws_sns_topic.test-pagerduty_alarm-non-critical-topic_3FD65796", + "pagerduty_service_integration.test-pagerduty_test-pagerduty_cloudwatch_8824B7B3-non-critical_496C437A" + ], + "endpoint": "https://events.pagerduty.com/integration/\${pagerduty_service_integration.test-pagerduty_test-pagerduty_cloudwatch_8824B7B3-non-critical_496C437A.integration_key}/enqueue", + "endpoint_auto_confirms": true, + "protocol": "https", + "topic_arn": "\${aws_sns_topic.test-pagerduty_alarm-non-critical-topic_3FD65796.arn}" + } + }, + "pagerduty_service": { + "test-pagerduty_pagerduty-critical_3EB714BD": { + "acknowledgement_timeout": "1800", + "alert_creation": "create_incidents", + "auto_resolve_timeout": "14400", + "description": "PagerDuty Critical", + "escalation_policy": "critical-id", + "incident_urgency_rule": { + "type": "constant", + "urgency": "high" + }, + "name": "Test-Env-PagerDuty-Critical" + }, + "test-pagerduty_pagerduty-non-critical_BBC21A2B": { + "acknowledgement_timeout": "1800", + "alert_creation": "create_incidents", + "auto_resolve_timeout": "14400", + "description": "PagerDuty Non-Critical", + "escalation_policy": "non-critical-id", + "incident_urgency_rule": { + "type": "constant", + "urgency": "low" + }, + "name": "Test-Env-PagerDuty-Non-Critical" + } + }, + "pagerduty_service_integration": { + "test-pagerduty_test-pagerduty_cloudwatch_8824B7B3-critical_29FC10B0": { + "depends_on": [ + "pagerduty_service.test-pagerduty_pagerduty-critical_3EB714BD" + ], + "name": "\${data.pagerduty_vendor.test-pagerduty_cloudwatch_8824B7B3.name}", + "service": "\${pagerduty_service.test-pagerduty_pagerduty-critical_3EB714BD.id}", + "vendor": "\${data.pagerduty_vendor.test-pagerduty_cloudwatch_8824B7B3.id}" + }, + "test-pagerduty_test-pagerduty_cloudwatch_8824B7B3-non-critical_496C437A": { + "depends_on": [ + "pagerduty_service.test-pagerduty_pagerduty-non-critical_BBC21A2B" + ], + "name": "\${data.pagerduty_vendor.test-pagerduty_cloudwatch_8824B7B3.name}", + "service": "\${pagerduty_service.test-pagerduty_pagerduty-non-critical_BBC21A2B.id}", + "vendor": "\${data.pagerduty_vendor.test-pagerduty_cloudwatch_8824B7B3.id}" + }, + "test-pagerduty_test-pagerduty_sentry_896D6C2C-critical_B5B900DE": { + "depends_on": [ + "pagerduty_service.test-pagerduty_pagerduty-critical_3EB714BD" + ], + "name": "\${data.pagerduty_vendor.test-pagerduty_sentry_896D6C2C.name}", + "service": "\${pagerduty_service.test-pagerduty_pagerduty-critical_3EB714BD.id}", + "vendor": "\${data.pagerduty_vendor.test-pagerduty_sentry_896D6C2C.id}" + }, + "test-pagerduty_test-pagerduty_sentry_896D6C2C-non-critical_15A9D0BD": { + "depends_on": [ + "pagerduty_service.test-pagerduty_pagerduty-non-critical_BBC21A2B" + ], + "name": "\${data.pagerduty_vendor.test-pagerduty_sentry_896D6C2C.name}", + "service": "\${pagerduty_service.test-pagerduty_pagerduty-non-critical_BBC21A2B.id}", + "vendor": "\${data.pagerduty_vendor.test-pagerduty_sentry_896D6C2C.id}" + } + } + } +}" +`; + +exports[`renders a Pocket PagerDuty with sns topic tags 1`] = ` +"{ + "data": { + "pagerduty_vendor": { + "test-pagerduty_cloudwatch_8824B7B3": { + "name": "Cloudwatch" + }, + "test-pagerduty_sentry_896D6C2C": { + "name": "Sentry" + } + } + }, + "resource": { + "aws_sns_topic": { + "test-pagerduty_alarm-critical-topic_5B8FEE8D": { + "name": "Test-Env-Infrastructure-Alarm-Critical", + "tags": { + "Test": "Topic" + } + }, + "test-pagerduty_alarm-non-critical-topic_3FD65796": { + "name": "Test-Env-Infrastructure-Alarm-Non-Critical", + "tags": { + "Test": "Topic" + } + } + }, + "aws_sns_topic_subscription": { + "test-pagerduty_alarm-critical-subscription_ABC78F4F": { + "confirmation_timeout_in_minutes": 2, + "depends_on": [ + "aws_sns_topic.test-pagerduty_alarm-critical-topic_5B8FEE8D", + "pagerduty_service_integration.test-pagerduty_test-pagerduty_cloudwatch_8824B7B3-critical_29FC10B0" + ], + "endpoint": "https://events.pagerduty.com/integration/\${pagerduty_service_integration.test-pagerduty_test-pagerduty_cloudwatch_8824B7B3-critical_29FC10B0.integration_key}/enqueue", + "endpoint_auto_confirms": true, + "protocol": "https", + "topic_arn": "\${aws_sns_topic.test-pagerduty_alarm-critical-topic_5B8FEE8D.arn}" + }, + "test-pagerduty_alarm-non-critical-subscription_D80993D9": { + "confirmation_timeout_in_minutes": 2, + "depends_on": [ + "aws_sns_topic.test-pagerduty_alarm-non-critical-topic_3FD65796", + "pagerduty_service_integration.test-pagerduty_test-pagerduty_cloudwatch_8824B7B3-non-critical_496C437A" + ], + "endpoint": "https://events.pagerduty.com/integration/\${pagerduty_service_integration.test-pagerduty_test-pagerduty_cloudwatch_8824B7B3-non-critical_496C437A.integration_key}/enqueue", + "endpoint_auto_confirms": true, + "protocol": "https", + "topic_arn": "\${aws_sns_topic.test-pagerduty_alarm-non-critical-topic_3FD65796.arn}" + } + }, + "pagerduty_service": { + "test-pagerduty_pagerduty-critical_3EB714BD": { + "acknowledgement_timeout": "1800", + "alert_creation": "create_incidents", + "auto_resolve_timeout": "14400", + "description": "PagerDuty Critical", + "escalation_policy": "critical-id", + "incident_urgency_rule": { + "type": "constant", + "urgency": "high" + }, + "name": "Test-Env-PagerDuty-Critical" + }, + "test-pagerduty_pagerduty-non-critical_BBC21A2B": { + "acknowledgement_timeout": "1800", + "alert_creation": "create_incidents", + "auto_resolve_timeout": "14400", + "description": "PagerDuty Non-Critical", + "escalation_policy": "non-critical-id", + "incident_urgency_rule": { + "type": "constant", + "urgency": "low" + }, + "name": "Test-Env-PagerDuty-Non-Critical" + } + }, + "pagerduty_service_integration": { + "test-pagerduty_test-pagerduty_cloudwatch_8824B7B3-critical_29FC10B0": { + "depends_on": [ + "pagerduty_service.test-pagerduty_pagerduty-critical_3EB714BD" + ], + "name": "\${data.pagerduty_vendor.test-pagerduty_cloudwatch_8824B7B3.name}", + "service": "\${pagerduty_service.test-pagerduty_pagerduty-critical_3EB714BD.id}", + "vendor": "\${data.pagerduty_vendor.test-pagerduty_cloudwatch_8824B7B3.id}" + }, + "test-pagerduty_test-pagerduty_cloudwatch_8824B7B3-non-critical_496C437A": { + "depends_on": [ + "pagerduty_service.test-pagerduty_pagerduty-non-critical_BBC21A2B" + ], + "name": "\${data.pagerduty_vendor.test-pagerduty_cloudwatch_8824B7B3.name}", + "service": "\${pagerduty_service.test-pagerduty_pagerduty-non-critical_BBC21A2B.id}", + "vendor": "\${data.pagerduty_vendor.test-pagerduty_cloudwatch_8824B7B3.id}" + }, + "test-pagerduty_test-pagerduty_sentry_896D6C2C-critical_B5B900DE": { + "depends_on": [ + "pagerduty_service.test-pagerduty_pagerduty-critical_3EB714BD" + ], + "name": "\${data.pagerduty_vendor.test-pagerduty_sentry_896D6C2C.name}", + "service": "\${pagerduty_service.test-pagerduty_pagerduty-critical_3EB714BD.id}", + "vendor": "\${data.pagerduty_vendor.test-pagerduty_sentry_896D6C2C.id}" + }, + "test-pagerduty_test-pagerduty_sentry_896D6C2C-non-critical_15A9D0BD": { + "depends_on": [ + "pagerduty_service.test-pagerduty_pagerduty-non-critical_BBC21A2B" + ], + "name": "\${data.pagerduty_vendor.test-pagerduty_sentry_896D6C2C.name}", + "service": "\${pagerduty_service.test-pagerduty_pagerduty-non-critical_BBC21A2B.id}", + "vendor": "\${data.pagerduty_vendor.test-pagerduty_sentry_896D6C2C.id}" + } + } + } +}" +`; diff --git a/packages/terraform-modules/src/pocket/__snapshots__/PocketSQSWithLambdaTarget.spec.ts.snap b/packages/terraform-modules/src/pocket/__snapshots__/PocketSQSWithLambdaTarget.spec.ts.snap new file mode 100644 index 00000000..6d9ed418 --- /dev/null +++ b/packages/terraform-modules/src/pocket/__snapshots__/PocketSQSWithLambdaTarget.spec.ts.snap @@ -0,0 +1,605 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`renders a lambda triggered by an existing sqs queue 1`] = ` +"{ + "data": { + "archive_file": { + "test-sqs-lambda_lambda-default-file_52E1CC8A": { + "output_path": "index.py.zip", + "source": [ + { + "content": "import json\\ndef handler(event, context):\\n\\t print(event)\\n\\t return {'statusCode': 200, 'headers': {'dance': 'party'}, 'body': json.dumps({'electric': 'boogaloo'}), 'isBase64Encoded': False}", + "filename": "index.py" + } + ], + "type": "zip" + } + }, + "aws_iam_policy_document": { + "test-sqs-lambda_assume-policy-document_98DE5C4D": { + "statement": [ + { + "actions": [ + "sts:AssumeRole" + ], + "effect": "Allow", + "principals": [ + { + "identifiers": [ + "lambda.amazonaws.com", + "edgelambda.amazonaws.com" + ], + "type": "Service" + } + ] + } + ], + "version": "2012-10-17" + }, + "test-sqs-lambda_execution-policy-document_2C5F0106": { + "statement": [ + { + "actions": [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents", + "logs:DescribeLogStreams" + ], + "effect": "Allow", + "resources": [ + "arn:aws:logs:*:*:*" + ] + } + ], + "version": "2012-10-17" + }, + "test-sqs-lambda_lambda_sqs_policy_3C63A136": { + "statement": [ + { + "actions": [ + "sqs:SendMessage", + "sqs:ReceiveMessage", + "sqs:DeleteMessage", + "sqs:GetQueueAttributes", + "sqs:ChangeMessageVisibility" + ], + "effect": "Allow", + "resources": [ + "\${data.aws_sqs_queue.test-sqs-lambda_lambda_sqs_queue_D097423A.arn}" + ] + } + ] + } + }, + "aws_sqs_queue": { + "test-sqs-lambda_lambda_sqs_queue_D097423A": { + "name": "my-existing-sqs" + } + } + }, + "resource": { + "aws_cloudwatch_log_group": { + "test-sqs-lambda_log-group_F69AB78F": { + "depends_on": [ + "aws_lambda_function.test-sqs-lambda_C2DB9DC9" + ], + "name": "/aws/lambda/\${aws_lambda_function.test-sqs-lambda_C2DB9DC9.function_name}", + "retention_in_days": 14 + } + }, + "aws_iam_policy": { + "test-sqs-lambda_execution-policy_ED32F1C0": { + "name": "test-sqs-lambda-ExecutionRolePolicy", + "policy": "\${data.aws_iam_policy_document.test-sqs-lambda_execution-policy-document_2C5F0106.json}" + }, + "test-sqs-lambda_sqs-policy_E98CC69F": { + "depends_on": [ + "aws_iam_role.test-sqs-lambda_execution-role_7323CCD9" + ], + "name": "test-sqs-lambda-LambdaSQSPolicy", + "policy": "\${data.aws_iam_policy_document.test-sqs-lambda_lambda_sqs_policy_3C63A136.json}" + } + }, + "aws_iam_role": { + "test-sqs-lambda_execution-role_7323CCD9": { + "assume_role_policy": "\${data.aws_iam_policy_document.test-sqs-lambda_assume-policy-document_98DE5C4D.json}", + "name": "test-sqs-lambda-ExecutionRole" + } + }, + "aws_iam_role_policy_attachment": { + "test-sqs-lambda_execution-role-policy-attachment_45759A7F": { + "depends_on": [ + "aws_iam_role.test-sqs-lambda_execution-role_7323CCD9", + "aws_iam_policy.test-sqs-lambda_execution-policy_ED32F1C0" + ], + "policy_arn": "\${aws_iam_policy.test-sqs-lambda_execution-policy_ED32F1C0.arn}", + "role": "\${aws_iam_role.test-sqs-lambda_execution-role_7323CCD9.name}" + }, + "test-sqs-lambda_execution-role-policy-attachment_CA21C309": { + "depends_on": [ + "aws_iam_role.test-sqs-lambda_execution-role_7323CCD9", + "aws_iam_policy.test-sqs-lambda_sqs-policy_E98CC69F" + ], + "policy_arn": "\${aws_iam_policy.test-sqs-lambda_sqs-policy_E98CC69F.arn}", + "role": "\${aws_iam_role.test-sqs-lambda_execution-role_7323CCD9.name}" + } + }, + "aws_lambda_alias": { + "test-sqs-lambda_alias_2C2A09A2": { + "depends_on": [ + "aws_lambda_function.test-sqs-lambda_C2DB9DC9" + ], + "function_name": "\${aws_lambda_function.test-sqs-lambda_C2DB9DC9.function_name}", + "function_version": "\${element(split(\\":\\", aws_lambda_function.test-sqs-lambda_C2DB9DC9.qualified_arn), 7)}", + "lifecycle": { + "ignore_changes": [ + "function_version" + ] + }, + "name": "DEPLOYED" + } + }, + "aws_lambda_event_source_mapping": { + "test-sqs-lambda_lambda_event_source_mapping_C60D5FF2": { + "event_source_arn": "\${data.aws_sqs_queue.test-sqs-lambda_lambda_sqs_queue_D097423A.arn}", + "function_name": "\${aws_lambda_alias.test-sqs-lambda_alias_2C2A09A2.arn}" + } + }, + "aws_lambda_function": { + "test-sqs-lambda_C2DB9DC9": { + "filename": "\${data.archive_file.test-sqs-lambda_lambda-default-file_52E1CC8A.output_path}", + "function_name": "test-sqs-lambda-Function", + "handler": "index.handler", + "lifecycle": { + "ignore_changes": [ + "filename", + "source_code_hash" + ] + }, + "memory_size": 128, + "publish": true, + "reserved_concurrent_executions": -1, + "role": "\${aws_iam_role.test-sqs-lambda_execution-role_7323CCD9.arn}", + "runtime": "python3.8", + "source_code_hash": "\${data.archive_file.test-sqs-lambda_lambda-default-file_52E1CC8A.output_base64sha256}", + "timeout": 5 + } + }, + "aws_s3_bucket": { + "test-sqs-lambda_code-bucket_34D549A2": { + "bucket": "pocket-test-sqs-lambda", + "force_destroy": true + } + }, + "aws_s3_bucket_acl": { + "test-sqs-lambda_code-bucket-acl_B5F709DD": { + "acl": "private", + "bucket": "\${aws_s3_bucket.test-sqs-lambda_code-bucket_34D549A2.id}", + "depends_on": [ + "aws_s3_bucket_ownership_controls.test-sqs-lambda_code-bucket-ownership-controls_1FE2FDA0" + ] + } + }, + "aws_s3_bucket_ownership_controls": { + "test-sqs-lambda_code-bucket-ownership-controls_1FE2FDA0": { + "bucket": "\${aws_s3_bucket.test-sqs-lambda_code-bucket_34D549A2.id}", + "rule": { + "object_ownership": "BucketOwnerPreferred" + } + } + }, + "aws_s3_bucket_public_access_block": { + "test-sqs-lambda_code-bucket-public-access-block_942034EC": { + "block_public_acls": true, + "block_public_policy": true, + "bucket": "\${aws_s3_bucket.test-sqs-lambda_code-bucket_34D549A2.id}" + } + } + } +}" +`; + +exports[`renders a plain sqs queue and lambda target 1`] = ` +"{ + "data": { + "archive_file": { + "test-sqs-lambda_lambda-default-file_52E1CC8A": { + "output_path": "index.py.zip", + "source": [ + { + "content": "import json\\ndef handler(event, context):\\n\\t print(event)\\n\\t return {'statusCode': 200, 'headers': {'dance': 'party'}, 'body': json.dumps({'electric': 'boogaloo'}), 'isBase64Encoded': False}", + "filename": "index.py" + } + ], + "type": "zip" + } + }, + "aws_iam_policy_document": { + "test-sqs-lambda_assume-policy-document_98DE5C4D": { + "statement": [ + { + "actions": [ + "sts:AssumeRole" + ], + "effect": "Allow", + "principals": [ + { + "identifiers": [ + "lambda.amazonaws.com", + "edgelambda.amazonaws.com" + ], + "type": "Service" + } + ] + } + ], + "version": "2012-10-17" + }, + "test-sqs-lambda_execution-policy-document_2C5F0106": { + "statement": [ + { + "actions": [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents", + "logs:DescribeLogStreams" + ], + "effect": "Allow", + "resources": [ + "arn:aws:logs:*:*:*" + ] + } + ], + "version": "2012-10-17" + }, + "test-sqs-lambda_lambda_sqs_policy_3C63A136": { + "statement": [ + { + "actions": [ + "sqs:SendMessage", + "sqs:ReceiveMessage", + "sqs:DeleteMessage", + "sqs:GetQueueAttributes", + "sqs:ChangeMessageVisibility" + ], + "effect": "Allow", + "resources": [ + "\${aws_sqs_queue.test-sqs-lambda_lambda_sqs_queue_7C6D24B3.arn}" + ] + } + ] + } + } + }, + "resource": { + "aws_cloudwatch_log_group": { + "test-sqs-lambda_log-group_F69AB78F": { + "depends_on": [ + "aws_lambda_function.test-sqs-lambda_C2DB9DC9" + ], + "name": "/aws/lambda/\${aws_lambda_function.test-sqs-lambda_C2DB9DC9.function_name}", + "retention_in_days": 14 + } + }, + "aws_iam_policy": { + "test-sqs-lambda_execution-policy_ED32F1C0": { + "name": "test-sqs-lambda-ExecutionRolePolicy", + "policy": "\${data.aws_iam_policy_document.test-sqs-lambda_execution-policy-document_2C5F0106.json}" + }, + "test-sqs-lambda_sqs-policy_E98CC69F": { + "depends_on": [ + "aws_iam_role.test-sqs-lambda_execution-role_7323CCD9" + ], + "name": "test-sqs-lambda-LambdaSQSPolicy", + "policy": "\${data.aws_iam_policy_document.test-sqs-lambda_lambda_sqs_policy_3C63A136.json}" + } + }, + "aws_iam_role": { + "test-sqs-lambda_execution-role_7323CCD9": { + "assume_role_policy": "\${data.aws_iam_policy_document.test-sqs-lambda_assume-policy-document_98DE5C4D.json}", + "name": "test-sqs-lambda-ExecutionRole" + } + }, + "aws_iam_role_policy_attachment": { + "test-sqs-lambda_execution-role-policy-attachment_45759A7F": { + "depends_on": [ + "aws_iam_role.test-sqs-lambda_execution-role_7323CCD9", + "aws_iam_policy.test-sqs-lambda_execution-policy_ED32F1C0" + ], + "policy_arn": "\${aws_iam_policy.test-sqs-lambda_execution-policy_ED32F1C0.arn}", + "role": "\${aws_iam_role.test-sqs-lambda_execution-role_7323CCD9.name}" + }, + "test-sqs-lambda_execution-role-policy-attachment_CA21C309": { + "depends_on": [ + "aws_iam_role.test-sqs-lambda_execution-role_7323CCD9", + "aws_iam_policy.test-sqs-lambda_sqs-policy_E98CC69F" + ], + "policy_arn": "\${aws_iam_policy.test-sqs-lambda_sqs-policy_E98CC69F.arn}", + "role": "\${aws_iam_role.test-sqs-lambda_execution-role_7323CCD9.name}" + } + }, + "aws_lambda_alias": { + "test-sqs-lambda_alias_2C2A09A2": { + "depends_on": [ + "aws_lambda_function.test-sqs-lambda_C2DB9DC9" + ], + "function_name": "\${aws_lambda_function.test-sqs-lambda_C2DB9DC9.function_name}", + "function_version": "\${element(split(\\":\\", aws_lambda_function.test-sqs-lambda_C2DB9DC9.qualified_arn), 7)}", + "lifecycle": { + "ignore_changes": [ + "function_version" + ] + }, + "name": "DEPLOYED" + } + }, + "aws_lambda_event_source_mapping": { + "test-sqs-lambda_lambda_event_source_mapping_C60D5FF2": { + "event_source_arn": "\${aws_sqs_queue.test-sqs-lambda_lambda_sqs_queue_7C6D24B3.arn}", + "function_name": "\${aws_lambda_alias.test-sqs-lambda_alias_2C2A09A2.arn}" + } + }, + "aws_lambda_function": { + "test-sqs-lambda_C2DB9DC9": { + "filename": "\${data.archive_file.test-sqs-lambda_lambda-default-file_52E1CC8A.output_path}", + "function_name": "test-sqs-lambda-Function", + "handler": "index.handler", + "lifecycle": { + "ignore_changes": [ + "filename", + "source_code_hash" + ] + }, + "memory_size": 128, + "publish": true, + "reserved_concurrent_executions": -1, + "role": "\${aws_iam_role.test-sqs-lambda_execution-role_7323CCD9.arn}", + "runtime": "python3.8", + "source_code_hash": "\${data.archive_file.test-sqs-lambda_lambda-default-file_52E1CC8A.output_base64sha256}", + "timeout": 5 + } + }, + "aws_s3_bucket": { + "test-sqs-lambda_code-bucket_34D549A2": { + "bucket": "pocket-test-sqs-lambda", + "force_destroy": true + } + }, + "aws_s3_bucket_acl": { + "test-sqs-lambda_code-bucket-acl_B5F709DD": { + "acl": "private", + "bucket": "\${aws_s3_bucket.test-sqs-lambda_code-bucket_34D549A2.id}", + "depends_on": [ + "aws_s3_bucket_ownership_controls.test-sqs-lambda_code-bucket-ownership-controls_1FE2FDA0" + ] + } + }, + "aws_s3_bucket_ownership_controls": { + "test-sqs-lambda_code-bucket-ownership-controls_1FE2FDA0": { + "bucket": "\${aws_s3_bucket.test-sqs-lambda_code-bucket_34D549A2.id}", + "rule": { + "object_ownership": "BucketOwnerPreferred" + } + } + }, + "aws_s3_bucket_public_access_block": { + "test-sqs-lambda_code-bucket-public-access-block_942034EC": { + "block_public_acls": true, + "block_public_policy": true, + "bucket": "\${aws_s3_bucket.test-sqs-lambda_code-bucket_34D549A2.id}" + } + }, + "aws_sqs_queue": { + "test-sqs-lambda_lambda_sqs_queue_7C6D24B3": { + "fifo_queue": false, + "name": "test-sqs-lambda-Queue" + } + } + } +}" +`; + +exports[`renders a plain sqs queue with a deadletter and lambda target 1`] = ` +"{ + "data": { + "archive_file": { + "test-sqs-lambda_lambda-default-file_52E1CC8A": { + "output_path": "index.py.zip", + "source": [ + { + "content": "import json\\ndef handler(event, context):\\n\\t print(event)\\n\\t return {'statusCode': 200, 'headers': {'dance': 'party'}, 'body': json.dumps({'electric': 'boogaloo'}), 'isBase64Encoded': False}", + "filename": "index.py" + } + ], + "type": "zip" + } + }, + "aws_iam_policy_document": { + "test-sqs-lambda_assume-policy-document_98DE5C4D": { + "statement": [ + { + "actions": [ + "sts:AssumeRole" + ], + "effect": "Allow", + "principals": [ + { + "identifiers": [ + "lambda.amazonaws.com", + "edgelambda.amazonaws.com" + ], + "type": "Service" + } + ] + } + ], + "version": "2012-10-17" + }, + "test-sqs-lambda_execution-policy-document_2C5F0106": { + "statement": [ + { + "actions": [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents", + "logs:DescribeLogStreams" + ], + "effect": "Allow", + "resources": [ + "arn:aws:logs:*:*:*" + ] + } + ], + "version": "2012-10-17" + }, + "test-sqs-lambda_lambda_sqs_policy_3C63A136": { + "statement": [ + { + "actions": [ + "sqs:SendMessage", + "sqs:ReceiveMessage", + "sqs:DeleteMessage", + "sqs:GetQueueAttributes", + "sqs:ChangeMessageVisibility" + ], + "effect": "Allow", + "resources": [ + "\${aws_sqs_queue.test-sqs-lambda_lambda_sqs_queue_7C6D24B3.arn}" + ] + } + ] + } + } + }, + "resource": { + "aws_cloudwatch_log_group": { + "test-sqs-lambda_log-group_F69AB78F": { + "depends_on": [ + "aws_lambda_function.test-sqs-lambda_C2DB9DC9" + ], + "name": "/aws/lambda/\${aws_lambda_function.test-sqs-lambda_C2DB9DC9.function_name}", + "retention_in_days": 14 + } + }, + "aws_iam_policy": { + "test-sqs-lambda_execution-policy_ED32F1C0": { + "name": "test-sqs-lambda-ExecutionRolePolicy", + "policy": "\${data.aws_iam_policy_document.test-sqs-lambda_execution-policy-document_2C5F0106.json}" + }, + "test-sqs-lambda_sqs-policy_E98CC69F": { + "depends_on": [ + "aws_iam_role.test-sqs-lambda_execution-role_7323CCD9" + ], + "name": "test-sqs-lambda-LambdaSQSPolicy", + "policy": "\${data.aws_iam_policy_document.test-sqs-lambda_lambda_sqs_policy_3C63A136.json}" + } + }, + "aws_iam_role": { + "test-sqs-lambda_execution-role_7323CCD9": { + "assume_role_policy": "\${data.aws_iam_policy_document.test-sqs-lambda_assume-policy-document_98DE5C4D.json}", + "name": "test-sqs-lambda-ExecutionRole" + } + }, + "aws_iam_role_policy_attachment": { + "test-sqs-lambda_execution-role-policy-attachment_45759A7F": { + "depends_on": [ + "aws_iam_role.test-sqs-lambda_execution-role_7323CCD9", + "aws_iam_policy.test-sqs-lambda_execution-policy_ED32F1C0" + ], + "policy_arn": "\${aws_iam_policy.test-sqs-lambda_execution-policy_ED32F1C0.arn}", + "role": "\${aws_iam_role.test-sqs-lambda_execution-role_7323CCD9.name}" + }, + "test-sqs-lambda_execution-role-policy-attachment_CA21C309": { + "depends_on": [ + "aws_iam_role.test-sqs-lambda_execution-role_7323CCD9", + "aws_iam_policy.test-sqs-lambda_sqs-policy_E98CC69F" + ], + "policy_arn": "\${aws_iam_policy.test-sqs-lambda_sqs-policy_E98CC69F.arn}", + "role": "\${aws_iam_role.test-sqs-lambda_execution-role_7323CCD9.name}" + } + }, + "aws_lambda_alias": { + "test-sqs-lambda_alias_2C2A09A2": { + "depends_on": [ + "aws_lambda_function.test-sqs-lambda_C2DB9DC9" + ], + "function_name": "\${aws_lambda_function.test-sqs-lambda_C2DB9DC9.function_name}", + "function_version": "\${element(split(\\":\\", aws_lambda_function.test-sqs-lambda_C2DB9DC9.qualified_arn), 7)}", + "lifecycle": { + "ignore_changes": [ + "function_version" + ] + }, + "name": "DEPLOYED" + } + }, + "aws_lambda_event_source_mapping": { + "test-sqs-lambda_lambda_event_source_mapping_C60D5FF2": { + "event_source_arn": "\${aws_sqs_queue.test-sqs-lambda_lambda_sqs_queue_7C6D24B3.arn}", + "function_name": "\${aws_lambda_alias.test-sqs-lambda_alias_2C2A09A2.arn}" + } + }, + "aws_lambda_function": { + "test-sqs-lambda_C2DB9DC9": { + "filename": "\${data.archive_file.test-sqs-lambda_lambda-default-file_52E1CC8A.output_path}", + "function_name": "test-sqs-lambda-Function", + "handler": "index.handler", + "lifecycle": { + "ignore_changes": [ + "filename", + "source_code_hash" + ] + }, + "memory_size": 128, + "publish": true, + "reserved_concurrent_executions": -1, + "role": "\${aws_iam_role.test-sqs-lambda_execution-role_7323CCD9.arn}", + "runtime": "python3.8", + "source_code_hash": "\${data.archive_file.test-sqs-lambda_lambda-default-file_52E1CC8A.output_base64sha256}", + "timeout": 5 + } + }, + "aws_s3_bucket": { + "test-sqs-lambda_code-bucket_34D549A2": { + "bucket": "pocket-test-sqs-lambda", + "force_destroy": true + } + }, + "aws_s3_bucket_acl": { + "test-sqs-lambda_code-bucket-acl_B5F709DD": { + "acl": "private", + "bucket": "\${aws_s3_bucket.test-sqs-lambda_code-bucket_34D549A2.id}", + "depends_on": [ + "aws_s3_bucket_ownership_controls.test-sqs-lambda_code-bucket-ownership-controls_1FE2FDA0" + ] + } + }, + "aws_s3_bucket_ownership_controls": { + "test-sqs-lambda_code-bucket-ownership-controls_1FE2FDA0": { + "bucket": "\${aws_s3_bucket.test-sqs-lambda_code-bucket_34D549A2.id}", + "rule": { + "object_ownership": "BucketOwnerPreferred" + } + } + }, + "aws_s3_bucket_public_access_block": { + "test-sqs-lambda_code-bucket-public-access-block_942034EC": { + "block_public_acls": true, + "block_public_policy": true, + "bucket": "\${aws_s3_bucket.test-sqs-lambda_code-bucket_34D549A2.id}" + } + }, + "aws_sqs_queue": { + "test-sqs-lambda_lambda_sqs_queue_7C6D24B3": { + "fifo_queue": false, + "name": "test-sqs-lambda-Queue", + "redrive_policy": "{\\"maxReceiveCount\\":3,\\"deadLetterTargetArn\\":\\"\${aws_sqs_queue.test-sqs-lambda_lambda_sqs_queue_redrive_sqs_queue_EBFF9A33.arn}\\"}" + }, + "test-sqs-lambda_lambda_sqs_queue_redrive_sqs_queue_EBFF9A33": { + "fifo_queue": false, + "name": "test-sqs-lambda-Queue-Deadletter" + } + } + } +}" +`; diff --git a/packages/terraform-modules/src/pocket/__snapshots__/PocketSynthetics.spec.ts.snap b/packages/terraform-modules/src/pocket/__snapshots__/PocketSynthetics.spec.ts.snap new file mode 100644 index 00000000..1e4f3fb3 --- /dev/null +++ b/packages/terraform-modules/src/pocket/__snapshots__/PocketSynthetics.spec.ts.snap @@ -0,0 +1,93 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`allows passing different values for nrql config 1`] = ` +"{ + "resource": { + "newrelic_nrql_alert_condition": { + "test-synthetic_alert-condition_4C991B53": { + "aggregation_delay": "180", + "aggregation_method": "cadence", + "aggregation_window": 3000, + "close_violations_on_expiration": true, + "critical": { + "operator": "above", + "threshold": 2, + "threshold_duration": 900, + "threshold_occurrences": "AT_LEAST_ONCE" + }, + "expiration_duration": 600, + "fill_option": "static", + "fill_value": 0, + "name": "test-synthetic-nrql", + "nrql": { + "query": "SELECT * FROM MY-COOL-TABLE" + }, + "policy_id": 1707149, + "slide_by": 60, + "violation_time_limit_seconds": 2592000 + } + }, + "newrelic_synthetics_monitor": { + "test-synthetic_test-synthetic-synthetics-monitor_914DC880": { + "locations_public": [ + "AWS_US_WEST_2", + "AWS_EU_WEST_2", + "AWS_US_EAST_2" + ], + "name": "test-synthetic-synthetics", + "period": "EVERY_5_MINUTES", + "status": "ENABLED", + "type": "SIMPLE", + "uri": "acme.getpocket.dev", + "verify_ssl": true + } + } + } +}" +`; + +exports[`renders a Pocket New Relic synthetic check 1`] = ` +"{ + "resource": { + "newrelic_nrql_alert_condition": { + "test-synthetic_alert-condition_4C991B53": { + "aggregation_delay": "180", + "aggregation_method": "cadence", + "aggregation_window": 3000, + "close_violations_on_expiration": true, + "critical": { + "operator": "above", + "threshold": 2, + "threshold_duration": 900, + "threshold_occurrences": "AT_LEAST_ONCE" + }, + "expiration_duration": 600, + "fill_option": "static", + "fill_value": 0, + "name": "test-synthetic-nrql", + "nrql": { + "query": "SELECT count(result) from SyntheticCheck where result = 'FAILED' and monitorName = '\${newrelic_synthetics_monitor.test-synthetic_test-synthetic-synthetics-monitor_914DC880.name}'" + }, + "policy_id": 1707149, + "slide_by": 60, + "violation_time_limit_seconds": 2592000 + } + }, + "newrelic_synthetics_monitor": { + "test-synthetic_test-synthetic-synthetics-monitor_914DC880": { + "locations_public": [ + "AWS_US_WEST_2", + "AWS_EU_WEST_2", + "AWS_US_EAST_2" + ], + "name": "test-synthetic-synthetics", + "period": "EVERY_5_MINUTES", + "status": "ENABLED", + "type": "SIMPLE", + "uri": "acme.getpocket.dev", + "verify_ssl": true + } + } + } +}" +`; diff --git a/packages/terraform-modules/src/pocket/__snapshots__/PocketVPC.spec.ts.snap b/packages/terraform-modules/src/pocket/__snapshots__/PocketVPC.spec.ts.snap new file mode 100644 index 00000000..e92a536f --- /dev/null +++ b/packages/terraform-modules/src/pocket/__snapshots__/PocketVPC.spec.ts.snap @@ -0,0 +1,108 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`renders a VPC with minimal config 1`] = ` +"{ + "data": { + "aws_caller_identity": { + "testPocketVPC_current_identity_433574E3": { + } + }, + "aws_kms_alias": { + "testPocketVPC_secrets_manager_key_3F5AECFC": { + "name": "alias/aws/secretsmanager" + } + }, + "aws_region": { + "testPocketVPC_current_region_1C1E28CD": { + } + }, + "aws_security_groups": { + "testPocketVPC_default_security_groups_48E7EA82": { + "filter": [ + { + "name": "group-name", + "values": [ + "default" + ] + }, + { + "name": "vpc-id", + "values": [ + "\${data.aws_vpc.testPocketVPC_vpc_52CB41F5.id}" + ] + } + ] + }, + "testPocketVPC_internal_security_groups_2506B665": { + "filter": [ + { + "name": "group-name", + "values": [ + "pocket-vpc-internal" + ] + }, + { + "name": "vpc-id", + "values": [ + "\${data.aws_vpc.testPocketVPC_vpc_52CB41F5.id}" + ] + } + ] + } + }, + "aws_ssm_parameter": { + "testPocketVPC_private_subnets_3CC52CDC": { + "name": "/Shared/PrivateSubnets" + }, + "testPocketVPC_public_subnets_21F01977": { + "name": "/Shared/PublicSubnets" + }, + "testPocketVPC_vpc_ssm_param_DED34ABC": { + "name": "/Shared/Vpc" + } + }, + "aws_subnets": { + "testPocketVPC_private_subnet_ids_61A307A3": { + "filter": [ + { + "name": "subnet-id", + "values": "\${split(\\",\\", data.aws_ssm_parameter.testPocketVPC_private_subnets_3CC52CDC.value)}" + }, + { + "name": "vpc-id", + "values": [ + "\${data.aws_vpc.testPocketVPC_vpc_52CB41F5.id}" + ] + } + ] + }, + "testPocketVPC_public_subnet_ids_5AC8BE89": { + "filter": [ + { + "name": "subnet-id", + "values": "\${split(\\",\\", data.aws_ssm_parameter.testPocketVPC_public_subnets_21F01977.value)}" + }, + { + "name": "vpc-id", + "values": [ + "\${data.aws_vpc.testPocketVPC_vpc_52CB41F5.id}" + ] + } + ] + } + }, + "aws_vpc": { + "testPocketVPC_vpc_52CB41F5": { + "filter": [ + { + "name": "vpc-id", + "values": [ + "\${data.aws_ssm_parameter.testPocketVPC_vpc_ssm_param_DED34ABC.value}" + ] + } + ] + } + } + } +}" +`; diff --git a/packages/terraform-modules/src/pocket/__snapshots__/PocketVersionedLambda.spec.ts.snap b/packages/terraform-modules/src/pocket/__snapshots__/PocketVersionedLambda.spec.ts.snap new file mode 100644 index 00000000..8a0e2df0 --- /dev/null +++ b/packages/terraform-modules/src/pocket/__snapshots__/PocketVersionedLambda.spec.ts.snap @@ -0,0 +1,2687 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`it can treat missing data as breaching 1`] = ` +"{ + "data": { + "archive_file": { + "test-lambda_lambda-default-file_7A7DBB68": { + "output_path": "index.py.zip", + "source": [ + { + "content": "import json\\ndef handler(event, context):\\n\\t print(event)\\n\\t return {'statusCode': 200, 'headers': {'dance': 'party'}, 'body': json.dumps({'electric': 'boogaloo'}), 'isBase64Encoded': False}", + "filename": "index.py" + } + ], + "type": "zip" + } + }, + "aws_iam_policy_document": { + "test-lambda_assume-policy-document_D538087D": { + "statement": [ + { + "actions": [ + "sts:AssumeRole" + ], + "effect": "Allow", + "principals": [ + { + "identifiers": [ + "lambda.amazonaws.com", + "edgelambda.amazonaws.com" + ], + "type": "Service" + } + ] + } + ], + "version": "2012-10-17" + }, + "test-lambda_execution-policy-document_D94D17B4": { + "statement": [ + { + "actions": [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents", + "logs:DescribeLogStreams" + ], + "effect": "Allow", + "resources": [ + "arn:aws:logs:*:*:*" + ] + } + ], + "version": "2012-10-17" + } + } + }, + "resource": { + "aws_cloudwatch_log_group": { + "test-lambda_log-group_ACC182B5": { + "depends_on": [ + "aws_lambda_function.test-lambda_8915D118" + ], + "name": "/aws/lambda/\${aws_lambda_function.test-lambda_8915D118.function_name}", + "retention_in_days": 14 + } + }, + "aws_cloudwatch_metric_alarm": { + "test-lambda_invocations_D15DE20B": { + "alarm_actions": [ + ], + "alarm_description": "Total invocations breaches threshold", + "alarm_name": "test-lambda-Lambda-Invocations-Alarm", + "comparison_operator": "GreaterThanThreshold", + "datapoints_to_alarm": 1, + "dimensions": { + "FunctionName": "\${aws_lambda_alias.test-lambda_alias_CCFDFCE9.function_name}", + "Resource": "\${aws_lambda_alias.test-lambda_alias_CCFDFCE9.function_name}:\${aws_lambda_alias.test-lambda_alias_CCFDFCE9.name}" + }, + "evaluation_periods": 1, + "insufficient_data_actions": [ + ], + "metric_name": "Invocations", + "namespace": "AWS/Lambda", + "ok_actions": [ + ], + "period": 60, + "statistic": "Sum", + "threshold": 1, + "treat_missing_data": "breaching" + } + }, + "aws_iam_policy": { + "test-lambda_execution-policy_19C785A8": { + "name": "test-lambda-ExecutionRolePolicy", + "policy": "\${data.aws_iam_policy_document.test-lambda_execution-policy-document_D94D17B4.json}" + } + }, + "aws_iam_role": { + "test-lambda_execution-role_9D4EE856": { + "assume_role_policy": "\${data.aws_iam_policy_document.test-lambda_assume-policy-document_D538087D.json}", + "name": "test-lambda-ExecutionRole" + } + }, + "aws_iam_role_policy_attachment": { + "test-lambda_execution-role-policy-attachment_8796505D": { + "depends_on": [ + "aws_iam_role.test-lambda_execution-role_9D4EE856", + "aws_iam_policy.test-lambda_execution-policy_19C785A8" + ], + "policy_arn": "\${aws_iam_policy.test-lambda_execution-policy_19C785A8.arn}", + "role": "\${aws_iam_role.test-lambda_execution-role_9D4EE856.name}" + } + }, + "aws_lambda_alias": { + "test-lambda_alias_CCFDFCE9": { + "depends_on": [ + "aws_lambda_function.test-lambda_8915D118" + ], + "function_name": "\${aws_lambda_function.test-lambda_8915D118.function_name}", + "function_version": "\${element(split(\\":\\", aws_lambda_function.test-lambda_8915D118.qualified_arn), 7)}", + "lifecycle": { + "ignore_changes": [ + "function_version" + ] + }, + "name": "DEPLOYED" + } + }, + "aws_lambda_function": { + "test-lambda_8915D118": { + "filename": "\${data.archive_file.test-lambda_lambda-default-file_7A7DBB68.output_path}", + "function_name": "test-lambda-Function", + "handler": "index.handler", + "lifecycle": { + "ignore_changes": [ + "filename", + "source_code_hash" + ] + }, + "memory_size": 128, + "publish": true, + "reserved_concurrent_executions": -1, + "role": "\${aws_iam_role.test-lambda_execution-role_9D4EE856.arn}", + "runtime": "python3.8", + "source_code_hash": "\${data.archive_file.test-lambda_lambda-default-file_7A7DBB68.output_base64sha256}", + "timeout": 5 + } + }, + "aws_s3_bucket": { + "test-lambda_code-bucket_177F316E": { + "bucket": "pocket-test-lambda", + "force_destroy": true + } + }, + "aws_s3_bucket_acl": { + "test-lambda_code-bucket-acl_C73BD0DB": { + "acl": "private", + "bucket": "\${aws_s3_bucket.test-lambda_code-bucket_177F316E.id}", + "depends_on": [ + "aws_s3_bucket_ownership_controls.test-lambda_code-bucket-ownership-controls_6A1865C3" + ] + } + }, + "aws_s3_bucket_ownership_controls": { + "test-lambda_code-bucket-ownership-controls_6A1865C3": { + "bucket": "\${aws_s3_bucket.test-lambda_code-bucket_177F316E.id}", + "rule": { + "object_ownership": "BucketOwnerPreferred" + } + } + }, + "aws_s3_bucket_public_access_block": { + "test-lambda_code-bucket-public-access-block_1E7CE5AE": { + "block_public_acls": true, + "block_public_policy": true, + "bucket": "\${aws_s3_bucket.test-lambda_code-bucket_177F316E.id}" + } + } + } +}" +`; + +exports[`renders a lambda target 1`] = ` +"{ + "data": { + "archive_file": { + "test-lambda_lambda-default-file_7A7DBB68": { + "output_path": "index.py.zip", + "source": [ + { + "content": "import json\\ndef handler(event, context):\\n\\t print(event)\\n\\t return {'statusCode': 200, 'headers': {'dance': 'party'}, 'body': json.dumps({'electric': 'boogaloo'}), 'isBase64Encoded': False}", + "filename": "index.py" + } + ], + "type": "zip" + } + }, + "aws_iam_policy_document": { + "test-lambda_assume-policy-document_D538087D": { + "statement": [ + { + "actions": [ + "sts:AssumeRole" + ], + "effect": "Allow", + "principals": [ + { + "identifiers": [ + "lambda.amazonaws.com", + "edgelambda.amazonaws.com" + ], + "type": "Service" + } + ] + } + ], + "version": "2012-10-17" + }, + "test-lambda_execution-policy-document_D94D17B4": { + "statement": [ + { + "actions": [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents", + "logs:DescribeLogStreams" + ], + "effect": "Allow", + "resources": [ + "arn:aws:logs:*:*:*" + ] + } + ], + "version": "2012-10-17" + } + } + }, + "resource": { + "aws_cloudwatch_log_group": { + "test-lambda_log-group_ACC182B5": { + "depends_on": [ + "aws_lambda_function.test-lambda_8915D118" + ], + "name": "/aws/lambda/\${aws_lambda_function.test-lambda_8915D118.function_name}", + "retention_in_days": 14 + } + }, + "aws_iam_policy": { + "test-lambda_execution-policy_19C785A8": { + "name": "test-lambda-ExecutionRolePolicy", + "policy": "\${data.aws_iam_policy_document.test-lambda_execution-policy-document_D94D17B4.json}" + } + }, + "aws_iam_role": { + "test-lambda_execution-role_9D4EE856": { + "assume_role_policy": "\${data.aws_iam_policy_document.test-lambda_assume-policy-document_D538087D.json}", + "name": "test-lambda-ExecutionRole" + } + }, + "aws_iam_role_policy_attachment": { + "test-lambda_execution-role-policy-attachment_8796505D": { + "depends_on": [ + "aws_iam_role.test-lambda_execution-role_9D4EE856", + "aws_iam_policy.test-lambda_execution-policy_19C785A8" + ], + "policy_arn": "\${aws_iam_policy.test-lambda_execution-policy_19C785A8.arn}", + "role": "\${aws_iam_role.test-lambda_execution-role_9D4EE856.name}" + } + }, + "aws_lambda_alias": { + "test-lambda_alias_CCFDFCE9": { + "depends_on": [ + "aws_lambda_function.test-lambda_8915D118" + ], + "function_name": "\${aws_lambda_function.test-lambda_8915D118.function_name}", + "function_version": "\${element(split(\\":\\", aws_lambda_function.test-lambda_8915D118.qualified_arn), 7)}", + "lifecycle": { + "ignore_changes": [ + "function_version" + ] + }, + "name": "DEPLOYED" + } + }, + "aws_lambda_function": { + "test-lambda_8915D118": { + "filename": "\${data.archive_file.test-lambda_lambda-default-file_7A7DBB68.output_path}", + "function_name": "test-lambda-Function", + "handler": "index.handler", + "lifecycle": { + "ignore_changes": [ + "filename", + "source_code_hash" + ] + }, + "memory_size": 128, + "publish": true, + "reserved_concurrent_executions": -1, + "role": "\${aws_iam_role.test-lambda_execution-role_9D4EE856.arn}", + "runtime": "python3.8", + "source_code_hash": "\${data.archive_file.test-lambda_lambda-default-file_7A7DBB68.output_base64sha256}", + "timeout": 5 + } + }, + "aws_s3_bucket": { + "test-lambda_code-bucket_177F316E": { + "bucket": "pocket-test-lambda", + "force_destroy": true + } + }, + "aws_s3_bucket_acl": { + "test-lambda_code-bucket-acl_C73BD0DB": { + "acl": "private", + "bucket": "\${aws_s3_bucket.test-lambda_code-bucket_177F316E.id}", + "depends_on": [ + "aws_s3_bucket_ownership_controls.test-lambda_code-bucket-ownership-controls_6A1865C3" + ] + } + }, + "aws_s3_bucket_ownership_controls": { + "test-lambda_code-bucket-ownership-controls_6A1865C3": { + "bucket": "\${aws_s3_bucket.test-lambda_code-bucket_177F316E.id}", + "rule": { + "object_ownership": "BucketOwnerPreferred" + } + } + }, + "aws_s3_bucket_public_access_block": { + "test-lambda_code-bucket-public-access-block_1E7CE5AE": { + "block_public_acls": true, + "block_public_policy": true, + "bucket": "\${aws_s3_bucket.test-lambda_code-bucket_177F316E.id}" + } + } + } +}" +`; + +exports[`renders a lambda target with tags 1`] = ` +"{ + "data": { + "archive_file": { + "test-lambda_lambda-default-file_7A7DBB68": { + "output_path": "index.py.zip", + "source": [ + { + "content": "import json\\ndef handler(event, context):\\n\\t print(event)\\n\\t return {'statusCode': 200, 'headers': {'dance': 'party'}, 'body': json.dumps({'electric': 'boogaloo'}), 'isBase64Encoded': False}", + "filename": "index.py" + } + ], + "type": "zip" + } + }, + "aws_iam_policy_document": { + "test-lambda_assume-policy-document_D538087D": { + "statement": [ + { + "actions": [ + "sts:AssumeRole" + ], + "effect": "Allow", + "principals": [ + { + "identifiers": [ + "lambda.amazonaws.com", + "edgelambda.amazonaws.com" + ], + "type": "Service" + } + ] + } + ], + "version": "2012-10-17" + }, + "test-lambda_execution-policy-document_D94D17B4": { + "statement": [ + { + "actions": [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents", + "logs:DescribeLogStreams" + ], + "effect": "Allow", + "resources": [ + "arn:aws:logs:*:*:*" + ] + } + ], + "version": "2012-10-17" + } + } + }, + "resource": { + "aws_cloudwatch_log_group": { + "test-lambda_log-group_ACC182B5": { + "depends_on": [ + "aws_lambda_function.test-lambda_8915D118" + ], + "name": "/aws/lambda/\${aws_lambda_function.test-lambda_8915D118.function_name}", + "retention_in_days": 14, + "tags": { + "for": "test", + "my": "tags" + } + } + }, + "aws_iam_policy": { + "test-lambda_execution-policy_19C785A8": { + "name": "test-lambda-ExecutionRolePolicy", + "policy": "\${data.aws_iam_policy_document.test-lambda_execution-policy-document_D94D17B4.json}", + "tags": { + "for": "test", + "my": "tags" + } + } + }, + "aws_iam_role": { + "test-lambda_execution-role_9D4EE856": { + "assume_role_policy": "\${data.aws_iam_policy_document.test-lambda_assume-policy-document_D538087D.json}", + "name": "test-lambda-ExecutionRole", + "tags": { + "for": "test", + "my": "tags" + } + } + }, + "aws_iam_role_policy_attachment": { + "test-lambda_execution-role-policy-attachment_8796505D": { + "depends_on": [ + "aws_iam_role.test-lambda_execution-role_9D4EE856", + "aws_iam_policy.test-lambda_execution-policy_19C785A8" + ], + "policy_arn": "\${aws_iam_policy.test-lambda_execution-policy_19C785A8.arn}", + "role": "\${aws_iam_role.test-lambda_execution-role_9D4EE856.name}" + } + }, + "aws_lambda_alias": { + "test-lambda_alias_CCFDFCE9": { + "depends_on": [ + "aws_lambda_function.test-lambda_8915D118" + ], + "function_name": "\${aws_lambda_function.test-lambda_8915D118.function_name}", + "function_version": "\${element(split(\\":\\", aws_lambda_function.test-lambda_8915D118.qualified_arn), 7)}", + "lifecycle": { + "ignore_changes": [ + "function_version" + ] + }, + "name": "DEPLOYED" + } + }, + "aws_lambda_function": { + "test-lambda_8915D118": { + "filename": "\${data.archive_file.test-lambda_lambda-default-file_7A7DBB68.output_path}", + "function_name": "test-lambda-Function", + "handler": "index.handler", + "lifecycle": { + "ignore_changes": [ + "filename", + "source_code_hash" + ] + }, + "memory_size": 128, + "publish": true, + "reserved_concurrent_executions": -1, + "role": "\${aws_iam_role.test-lambda_execution-role_9D4EE856.arn}", + "runtime": "python3.8", + "source_code_hash": "\${data.archive_file.test-lambda_lambda-default-file_7A7DBB68.output_base64sha256}", + "tags": { + "for": "test", + "my": "tags" + }, + "timeout": 5 + } + }, + "aws_s3_bucket": { + "test-lambda_code-bucket_177F316E": { + "bucket": "pocket-test-lambda", + "force_destroy": true, + "tags": { + "for": "test", + "my": "tags" + } + } + }, + "aws_s3_bucket_acl": { + "test-lambda_code-bucket-acl_C73BD0DB": { + "acl": "private", + "bucket": "\${aws_s3_bucket.test-lambda_code-bucket_177F316E.id}", + "depends_on": [ + "aws_s3_bucket_ownership_controls.test-lambda_code-bucket-ownership-controls_6A1865C3" + ] + } + }, + "aws_s3_bucket_ownership_controls": { + "test-lambda_code-bucket-ownership-controls_6A1865C3": { + "bucket": "\${aws_s3_bucket.test-lambda_code-bucket_177F316E.id}", + "rule": { + "object_ownership": "BucketOwnerPreferred" + } + } + }, + "aws_s3_bucket_public_access_block": { + "test-lambda_code-bucket-public-access-block_1E7CE5AE": { + "block_public_acls": true, + "block_public_policy": true, + "bucket": "\${aws_s3_bucket.test-lambda_code-bucket_177F316E.id}" + } + } + } +}" +`; + +exports[`renders a lambda with alarms 1`] = ` +"{ + "data": { + "archive_file": { + "test-lambda_lambda-default-file_7A7DBB68": { + "output_path": "index.py.zip", + "source": [ + { + "content": "import json\\ndef handler(event, context):\\n\\t print(event)\\n\\t return {'statusCode': 200, 'headers': {'dance': 'party'}, 'body': json.dumps({'electric': 'boogaloo'}), 'isBase64Encoded': False}", + "filename": "index.py" + } + ], + "type": "zip" + } + }, + "aws_iam_policy_document": { + "test-lambda_assume-policy-document_D538087D": { + "statement": [ + { + "actions": [ + "sts:AssumeRole" + ], + "effect": "Allow", + "principals": [ + { + "identifiers": [ + "lambda.amazonaws.com", + "edgelambda.amazonaws.com" + ], + "type": "Service" + } + ] + } + ], + "version": "2012-10-17" + }, + "test-lambda_execution-policy-document_D94D17B4": { + "statement": [ + { + "actions": [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents", + "logs:DescribeLogStreams" + ], + "effect": "Allow", + "resources": [ + "arn:aws:logs:*:*:*" + ] + } + ], + "version": "2012-10-17" + } + } + }, + "resource": { + "aws_cloudwatch_log_group": { + "test-lambda_log-group_ACC182B5": { + "depends_on": [ + "aws_lambda_function.test-lambda_8915D118" + ], + "name": "/aws/lambda/\${aws_lambda_function.test-lambda_8915D118.function_name}", + "retention_in_days": 14 + } + }, + "aws_cloudwatch_metric_alarm": { + "test-lambda_concurrentexecutions_B7910145": { + "alarm_actions": [ + ], + "alarm_description": "Total concurrentexecutions breaches threshold", + "alarm_name": "test-lambda-Lambda-ConcurrentExecutions-Alarm", + "comparison_operator": "GreaterThanThreshold", + "datapoints_to_alarm": 1, + "dimensions": { + "FunctionName": "\${aws_lambda_alias.test-lambda_alias_CCFDFCE9.function_name}", + "Resource": "\${aws_lambda_alias.test-lambda_alias_CCFDFCE9.function_name}:\${aws_lambda_alias.test-lambda_alias_CCFDFCE9.name}" + }, + "evaluation_periods": 1, + "insufficient_data_actions": [ + ], + "metric_name": "ConcurrentExecutions", + "namespace": "AWS/Lambda", + "ok_actions": [ + ], + "period": 60, + "statistic": "Sum", + "threshold": 1, + "treat_missing_data": "missing" + }, + "test-lambda_duration_E92A1EBB": { + "alarm_actions": [ + ], + "alarm_description": "Total duration breaches threshold", + "alarm_name": "test-lambda-Lambda-Duration-Alarm", + "comparison_operator": "GreaterThanThreshold", + "datapoints_to_alarm": 1, + "dimensions": { + "FunctionName": "\${aws_lambda_alias.test-lambda_alias_CCFDFCE9.function_name}", + "Resource": "\${aws_lambda_alias.test-lambda_alias_CCFDFCE9.function_name}:\${aws_lambda_alias.test-lambda_alias_CCFDFCE9.name}" + }, + "evaluation_periods": 1, + "insufficient_data_actions": [ + ], + "metric_name": "Duration", + "namespace": "AWS/Lambda", + "ok_actions": [ + ], + "period": 60, + "statistic": "Sum", + "threshold": 1, + "treat_missing_data": "missing" + }, + "test-lambda_errors_E579F2B3": { + "alarm_actions": [ + ], + "alarm_description": "Total errors breaches threshold", + "alarm_name": "test-lambda-Lambda-Errors-Alarm", + "comparison_operator": "GreaterThanThreshold", + "datapoints_to_alarm": 1, + "dimensions": { + "FunctionName": "\${aws_lambda_alias.test-lambda_alias_CCFDFCE9.function_name}", + "Resource": "\${aws_lambda_alias.test-lambda_alias_CCFDFCE9.function_name}:\${aws_lambda_alias.test-lambda_alias_CCFDFCE9.name}" + }, + "evaluation_periods": 1, + "insufficient_data_actions": [ + ], + "metric_name": "Errors", + "namespace": "AWS/Lambda", + "ok_actions": [ + ], + "period": 60, + "statistic": "Sum", + "threshold": 1, + "treat_missing_data": "missing" + }, + "test-lambda_invocations_D15DE20B": { + "alarm_actions": [ + ], + "alarm_description": "Total invocations breaches threshold", + "alarm_name": "test-lambda-Lambda-Invocations-Alarm", + "comparison_operator": "GreaterThanThreshold", + "datapoints_to_alarm": 1, + "dimensions": { + "FunctionName": "\${aws_lambda_alias.test-lambda_alias_CCFDFCE9.function_name}", + "Resource": "\${aws_lambda_alias.test-lambda_alias_CCFDFCE9.function_name}:\${aws_lambda_alias.test-lambda_alias_CCFDFCE9.name}" + }, + "evaluation_periods": 1, + "insufficient_data_actions": [ + ], + "metric_name": "Invocations", + "namespace": "AWS/Lambda", + "ok_actions": [ + ], + "period": 60, + "statistic": "Sum", + "threshold": 1, + "treat_missing_data": "missing" + }, + "test-lambda_throttles_562A3BCE": { + "alarm_actions": [ + ], + "alarm_description": "Total throttles breaches threshold", + "alarm_name": "test-lambda-Lambda-Throttles-Alarm", + "comparison_operator": "GreaterThanThreshold", + "datapoints_to_alarm": 1, + "dimensions": { + "FunctionName": "\${aws_lambda_alias.test-lambda_alias_CCFDFCE9.function_name}", + "Resource": "\${aws_lambda_alias.test-lambda_alias_CCFDFCE9.function_name}:\${aws_lambda_alias.test-lambda_alias_CCFDFCE9.name}" + }, + "evaluation_periods": 1, + "insufficient_data_actions": [ + ], + "metric_name": "Throttles", + "namespace": "AWS/Lambda", + "ok_actions": [ + ], + "period": 60, + "statistic": "Sum", + "threshold": 1, + "treat_missing_data": "missing" + } + }, + "aws_iam_policy": { + "test-lambda_execution-policy_19C785A8": { + "name": "test-lambda-ExecutionRolePolicy", + "policy": "\${data.aws_iam_policy_document.test-lambda_execution-policy-document_D94D17B4.json}" + } + }, + "aws_iam_role": { + "test-lambda_execution-role_9D4EE856": { + "assume_role_policy": "\${data.aws_iam_policy_document.test-lambda_assume-policy-document_D538087D.json}", + "name": "test-lambda-ExecutionRole" + } + }, + "aws_iam_role_policy_attachment": { + "test-lambda_execution-role-policy-attachment_8796505D": { + "depends_on": [ + "aws_iam_role.test-lambda_execution-role_9D4EE856", + "aws_iam_policy.test-lambda_execution-policy_19C785A8" + ], + "policy_arn": "\${aws_iam_policy.test-lambda_execution-policy_19C785A8.arn}", + "role": "\${aws_iam_role.test-lambda_execution-role_9D4EE856.name}" + } + }, + "aws_lambda_alias": { + "test-lambda_alias_CCFDFCE9": { + "depends_on": [ + "aws_lambda_function.test-lambda_8915D118" + ], + "function_name": "\${aws_lambda_function.test-lambda_8915D118.function_name}", + "function_version": "\${element(split(\\":\\", aws_lambda_function.test-lambda_8915D118.qualified_arn), 7)}", + "lifecycle": { + "ignore_changes": [ + "function_version" + ] + }, + "name": "DEPLOYED" + } + }, + "aws_lambda_function": { + "test-lambda_8915D118": { + "filename": "\${data.archive_file.test-lambda_lambda-default-file_7A7DBB68.output_path}", + "function_name": "test-lambda-Function", + "handler": "index.handler", + "lifecycle": { + "ignore_changes": [ + "filename", + "source_code_hash" + ] + }, + "memory_size": 128, + "publish": true, + "reserved_concurrent_executions": -1, + "role": "\${aws_iam_role.test-lambda_execution-role_9D4EE856.arn}", + "runtime": "python3.8", + "source_code_hash": "\${data.archive_file.test-lambda_lambda-default-file_7A7DBB68.output_base64sha256}", + "timeout": 5 + } + }, + "aws_s3_bucket": { + "test-lambda_code-bucket_177F316E": { + "bucket": "pocket-test-lambda", + "force_destroy": true + } + }, + "aws_s3_bucket_acl": { + "test-lambda_code-bucket-acl_C73BD0DB": { + "acl": "private", + "bucket": "\${aws_s3_bucket.test-lambda_code-bucket_177F316E.id}", + "depends_on": [ + "aws_s3_bucket_ownership_controls.test-lambda_code-bucket-ownership-controls_6A1865C3" + ] + } + }, + "aws_s3_bucket_ownership_controls": { + "test-lambda_code-bucket-ownership-controls_6A1865C3": { + "bucket": "\${aws_s3_bucket.test-lambda_code-bucket_177F316E.id}", + "rule": { + "object_ownership": "BucketOwnerPreferred" + } + } + }, + "aws_s3_bucket_public_access_block": { + "test-lambda_code-bucket-public-access-block_1E7CE5AE": { + "block_public_acls": true, + "block_public_policy": true, + "bucket": "\${aws_s3_bucket.test-lambda_code-bucket_177F316E.id}" + } + } + } +}" +`; + +exports[`renders a lambda with code deploy 1`] = ` +"{ + "data": { + "archive_file": { + "test-lambda_lambda-default-file_7A7DBB68": { + "output_path": "index.py.zip", + "source": [ + { + "content": "import json\\ndef handler(event, context):\\n\\t print(event)\\n\\t return {'statusCode': 200, 'headers': {'dance': 'party'}, 'body': json.dumps({'electric': 'boogaloo'}), 'isBase64Encoded': False}", + "filename": "index.py" + } + ], + "type": "zip" + } + }, + "aws_iam_policy_document": { + "test-lambda_assume-policy-document_D538087D": { + "statement": [ + { + "actions": [ + "sts:AssumeRole" + ], + "effect": "Allow", + "principals": [ + { + "identifiers": [ + "lambda.amazonaws.com", + "edgelambda.amazonaws.com" + ], + "type": "Service" + } + ] + } + ], + "version": "2012-10-17" + }, + "test-lambda_execution-policy-document_D94D17B4": { + "statement": [ + { + "actions": [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents", + "logs:DescribeLogStreams" + ], + "effect": "Allow", + "resources": [ + "arn:aws:logs:*:*:*" + ] + } + ], + "version": "2012-10-17" + }, + "test-lambda_lambda-code-deploy_code-deploy-assume-role-policy-document_C6C06F39": { + "statement": [ + { + "actions": [ + "sts:AssumeRole" + ], + "effect": "Allow", + "principals": [ + { + "identifiers": [ + "codedeploy.amazonaws.com" + ], + "type": "Service" + } + ] + } + ] + } + } + }, + "resource": { + "aws_cloudwatch_log_group": { + "test-lambda_log-group_ACC182B5": { + "depends_on": [ + "aws_lambda_function.test-lambda_8915D118" + ], + "name": "/aws/lambda/\${aws_lambda_function.test-lambda_8915D118.function_name}", + "retention_in_days": 14 + } + }, + "aws_codedeploy_app": { + "test-lambda_lambda-code-deploy_code-deploy-app_7C98647C": { + "compute_platform": "Lambda", + "name": "test-lambda-Lambda" + } + }, + "aws_codedeploy_deployment_group": { + "test-lambda_lambda-code-deploy_code-deployment-group_07908CD9": { + "app_name": "\${aws_codedeploy_app.test-lambda_lambda-code-deploy_code-deploy-app_7C98647C.name}", + "auto_rollback_configuration": { + "enabled": true, + "events": [ + "DEPLOYMENT_FAILURE" + ] + }, + "depends_on": [ + "aws_codedeploy_app.test-lambda_lambda-code-deploy_code-deploy-app_7C98647C" + ], + "deployment_config_name": "CodeDeployDefault.LambdaAllAtOnce", + "deployment_group_name": "\${aws_codedeploy_app.test-lambda_lambda-code-deploy_code-deploy-app_7C98647C.name}", + "deployment_style": { + "deployment_option": "WITH_TRAFFIC_CONTROL", + "deployment_type": "BLUE_GREEN" + }, + "service_role_arn": "\${aws_iam_role.test-lambda_lambda-code-deploy_code-deploy-role_6EA86ECC.arn}" + } + }, + "aws_codestarnotifications_notification_rule": { + "test-lambda_lambda-code-deploy_notifications_E3D09B45": { + "detail_type": "FULL", + "event_type_ids": [ + "codedeploy-application-deployment-failed" + ], + "name": "\${aws_codedeploy_app.test-lambda_lambda-code-deploy_code-deploy-app_7C98647C.name}", + "resource": "arn:aws:codedeploy:us-east-1:test-account:application:\${aws_codedeploy_app.test-lambda_lambda-code-deploy_code-deploy-app_7C98647C.name}", + "target": [ + { + "address": "arn:test" + } + ] + } + }, + "aws_iam_policy": { + "test-lambda_execution-policy_19C785A8": { + "name": "test-lambda-ExecutionRolePolicy", + "policy": "\${data.aws_iam_policy_document.test-lambda_execution-policy-document_D94D17B4.json}" + } + }, + "aws_iam_role": { + "test-lambda_execution-role_9D4EE856": { + "assume_role_policy": "\${data.aws_iam_policy_document.test-lambda_assume-policy-document_D538087D.json}", + "name": "test-lambda-ExecutionRole" + }, + "test-lambda_lambda-code-deploy_code-deploy-role_6EA86ECC": { + "assume_role_policy": "\${data.aws_iam_policy_document.test-lambda_lambda-code-deploy_code-deploy-assume-role-policy-document_C6C06F39.json}", + "name": "test-lambda-CodeDeployRole" + } + }, + "aws_iam_role_policy_attachment": { + "test-lambda_execution-role-policy-attachment_8796505D": { + "depends_on": [ + "aws_iam_role.test-lambda_execution-role_9D4EE856", + "aws_iam_policy.test-lambda_execution-policy_19C785A8" + ], + "policy_arn": "\${aws_iam_policy.test-lambda_execution-policy_19C785A8.arn}", + "role": "\${aws_iam_role.test-lambda_execution-role_9D4EE856.name}" + }, + "test-lambda_lambda-code-deploy_code-deploy-policy-attachment_0805E344": { + "depends_on": [ + "aws_iam_role.test-lambda_lambda-code-deploy_code-deploy-role_6EA86ECC" + ], + "policy_arn": "arn:aws:iam::aws:policy/service-role/AWSCodeDeployRoleForLambda", + "role": "\${aws_iam_role.test-lambda_lambda-code-deploy_code-deploy-role_6EA86ECC.name}" + } + }, + "aws_lambda_alias": { + "test-lambda_alias_CCFDFCE9": { + "depends_on": [ + "aws_lambda_function.test-lambda_8915D118" + ], + "function_name": "\${aws_lambda_function.test-lambda_8915D118.function_name}", + "function_version": "\${element(split(\\":\\", aws_lambda_function.test-lambda_8915D118.qualified_arn), 7)}", + "lifecycle": { + "ignore_changes": [ + "function_version" + ] + }, + "name": "DEPLOYED" + } + }, + "aws_lambda_function": { + "test-lambda_8915D118": { + "filename": "\${data.archive_file.test-lambda_lambda-default-file_7A7DBB68.output_path}", + "function_name": "test-lambda-Function", + "handler": "index.handler", + "lifecycle": { + "ignore_changes": [ + "filename", + "source_code_hash", + "publish" + ] + }, + "memory_size": 128, + "publish": true, + "reserved_concurrent_executions": -1, + "role": "\${aws_iam_role.test-lambda_execution-role_9D4EE856.arn}", + "runtime": "python3.8", + "source_code_hash": "\${data.archive_file.test-lambda_lambda-default-file_7A7DBB68.output_base64sha256}", + "timeout": 5 + } + }, + "aws_s3_bucket": { + "test-lambda_code-bucket_177F316E": { + "bucket": "pocket-test-lambda", + "force_destroy": true + } + }, + "aws_s3_bucket_acl": { + "test-lambda_code-bucket-acl_C73BD0DB": { + "acl": "private", + "bucket": "\${aws_s3_bucket.test-lambda_code-bucket_177F316E.id}", + "depends_on": [ + "aws_s3_bucket_ownership_controls.test-lambda_code-bucket-ownership-controls_6A1865C3" + ] + } + }, + "aws_s3_bucket_ownership_controls": { + "test-lambda_code-bucket-ownership-controls_6A1865C3": { + "bucket": "\${aws_s3_bucket.test-lambda_code-bucket_177F316E.id}", + "rule": { + "object_ownership": "BucketOwnerPreferred" + } + } + }, + "aws_s3_bucket_public_access_block": { + "test-lambda_code-bucket-public-access-block_1E7CE5AE": { + "block_public_acls": true, + "block_public_policy": true, + "bucket": "\${aws_s3_bucket.test-lambda_code-bucket_177F316E.id}" + } + } + } +}" +`; + +exports[`renders a lambda with code deploy with all deploy notifications turned on 1`] = ` +"{ + "data": { + "archive_file": { + "test-lambda_lambda-default-file_7A7DBB68": { + "output_path": "index.py.zip", + "source": [ + { + "content": "import json\\ndef handler(event, context):\\n\\t print(event)\\n\\t return {'statusCode': 200, 'headers': {'dance': 'party'}, 'body': json.dumps({'electric': 'boogaloo'}), 'isBase64Encoded': False}", + "filename": "index.py" + } + ], + "type": "zip" + } + }, + "aws_iam_policy_document": { + "test-lambda_assume-policy-document_D538087D": { + "statement": [ + { + "actions": [ + "sts:AssumeRole" + ], + "effect": "Allow", + "principals": [ + { + "identifiers": [ + "lambda.amazonaws.com", + "edgelambda.amazonaws.com" + ], + "type": "Service" + } + ] + } + ], + "version": "2012-10-17" + }, + "test-lambda_execution-policy-document_D94D17B4": { + "statement": [ + { + "actions": [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents", + "logs:DescribeLogStreams" + ], + "effect": "Allow", + "resources": [ + "arn:aws:logs:*:*:*" + ] + } + ], + "version": "2012-10-17" + }, + "test-lambda_lambda-code-deploy_code-deploy-assume-role-policy-document_C6C06F39": { + "statement": [ + { + "actions": [ + "sts:AssumeRole" + ], + "effect": "Allow", + "principals": [ + { + "identifiers": [ + "codedeploy.amazonaws.com" + ], + "type": "Service" + } + ] + } + ] + } + } + }, + "resource": { + "aws_cloudwatch_log_group": { + "test-lambda_log-group_ACC182B5": { + "depends_on": [ + "aws_lambda_function.test-lambda_8915D118" + ], + "name": "/aws/lambda/\${aws_lambda_function.test-lambda_8915D118.function_name}", + "retention_in_days": 14 + } + }, + "aws_codedeploy_app": { + "test-lambda_lambda-code-deploy_code-deploy-app_7C98647C": { + "compute_platform": "Lambda", + "name": "test-lambda-Lambda" + } + }, + "aws_codedeploy_deployment_group": { + "test-lambda_lambda-code-deploy_code-deployment-group_07908CD9": { + "app_name": "\${aws_codedeploy_app.test-lambda_lambda-code-deploy_code-deploy-app_7C98647C.name}", + "auto_rollback_configuration": { + "enabled": true, + "events": [ + "DEPLOYMENT_FAILURE" + ] + }, + "depends_on": [ + "aws_codedeploy_app.test-lambda_lambda-code-deploy_code-deploy-app_7C98647C" + ], + "deployment_config_name": "CodeDeployDefault.LambdaAllAtOnce", + "deployment_group_name": "\${aws_codedeploy_app.test-lambda_lambda-code-deploy_code-deploy-app_7C98647C.name}", + "deployment_style": { + "deployment_option": "WITH_TRAFFIC_CONTROL", + "deployment_type": "BLUE_GREEN" + }, + "service_role_arn": "\${aws_iam_role.test-lambda_lambda-code-deploy_code-deploy-role_6EA86ECC.arn}" + } + }, + "aws_codestarnotifications_notification_rule": { + "test-lambda_lambda-code-deploy_notifications_E3D09B45": { + "detail_type": "FULL", + "event_type_ids": [ + "codedeploy-application-deployment-failed", + "codedeploy-application-deployment-succeeded", + "codedeploy-application-deployment-started" + ], + "name": "\${aws_codedeploy_app.test-lambda_lambda-code-deploy_code-deploy-app_7C98647C.name}", + "resource": "arn:aws:codedeploy:us-east-1:test-account:application:\${aws_codedeploy_app.test-lambda_lambda-code-deploy_code-deploy-app_7C98647C.name}", + "target": [ + { + "address": "arn:test" + } + ] + } + }, + "aws_iam_policy": { + "test-lambda_execution-policy_19C785A8": { + "name": "test-lambda-ExecutionRolePolicy", + "policy": "\${data.aws_iam_policy_document.test-lambda_execution-policy-document_D94D17B4.json}" + } + }, + "aws_iam_role": { + "test-lambda_execution-role_9D4EE856": { + "assume_role_policy": "\${data.aws_iam_policy_document.test-lambda_assume-policy-document_D538087D.json}", + "name": "test-lambda-ExecutionRole" + }, + "test-lambda_lambda-code-deploy_code-deploy-role_6EA86ECC": { + "assume_role_policy": "\${data.aws_iam_policy_document.test-lambda_lambda-code-deploy_code-deploy-assume-role-policy-document_C6C06F39.json}", + "name": "test-lambda-CodeDeployRole" + } + }, + "aws_iam_role_policy_attachment": { + "test-lambda_execution-role-policy-attachment_8796505D": { + "depends_on": [ + "aws_iam_role.test-lambda_execution-role_9D4EE856", + "aws_iam_policy.test-lambda_execution-policy_19C785A8" + ], + "policy_arn": "\${aws_iam_policy.test-lambda_execution-policy_19C785A8.arn}", + "role": "\${aws_iam_role.test-lambda_execution-role_9D4EE856.name}" + }, + "test-lambda_lambda-code-deploy_code-deploy-policy-attachment_0805E344": { + "depends_on": [ + "aws_iam_role.test-lambda_lambda-code-deploy_code-deploy-role_6EA86ECC" + ], + "policy_arn": "arn:aws:iam::aws:policy/service-role/AWSCodeDeployRoleForLambda", + "role": "\${aws_iam_role.test-lambda_lambda-code-deploy_code-deploy-role_6EA86ECC.name}" + } + }, + "aws_lambda_alias": { + "test-lambda_alias_CCFDFCE9": { + "depends_on": [ + "aws_lambda_function.test-lambda_8915D118" + ], + "function_name": "\${aws_lambda_function.test-lambda_8915D118.function_name}", + "function_version": "\${element(split(\\":\\", aws_lambda_function.test-lambda_8915D118.qualified_arn), 7)}", + "lifecycle": { + "ignore_changes": [ + "function_version" + ] + }, + "name": "DEPLOYED" + } + }, + "aws_lambda_function": { + "test-lambda_8915D118": { + "filename": "\${data.archive_file.test-lambda_lambda-default-file_7A7DBB68.output_path}", + "function_name": "test-lambda-Function", + "handler": "index.handler", + "lifecycle": { + "ignore_changes": [ + "filename", + "source_code_hash", + "publish" + ] + }, + "memory_size": 128, + "publish": true, + "reserved_concurrent_executions": -1, + "role": "\${aws_iam_role.test-lambda_execution-role_9D4EE856.arn}", + "runtime": "python3.8", + "source_code_hash": "\${data.archive_file.test-lambda_lambda-default-file_7A7DBB68.output_base64sha256}", + "timeout": 5 + } + }, + "aws_s3_bucket": { + "test-lambda_code-bucket_177F316E": { + "bucket": "pocket-test-lambda", + "force_destroy": true + } + }, + "aws_s3_bucket_acl": { + "test-lambda_code-bucket-acl_C73BD0DB": { + "acl": "private", + "bucket": "\${aws_s3_bucket.test-lambda_code-bucket_177F316E.id}", + "depends_on": [ + "aws_s3_bucket_ownership_controls.test-lambda_code-bucket-ownership-controls_6A1865C3" + ] + } + }, + "aws_s3_bucket_ownership_controls": { + "test-lambda_code-bucket-ownership-controls_6A1865C3": { + "bucket": "\${aws_s3_bucket.test-lambda_code-bucket_177F316E.id}", + "rule": { + "object_ownership": "BucketOwnerPreferred" + } + } + }, + "aws_s3_bucket_public_access_block": { + "test-lambda_code-bucket-public-access-block_1E7CE5AE": { + "block_public_acls": true, + "block_public_policy": true, + "bucket": "\${aws_s3_bucket.test-lambda_code-bucket_177F316E.id}" + } + } + } +}" +`; + +exports[`renders a lambda with concurrencyLimit 1`] = ` +"{ + "data": { + "archive_file": { + "test-lambda_lambda-default-file_7A7DBB68": { + "output_path": "index.py.zip", + "source": [ + { + "content": "import json\\ndef handler(event, context):\\n\\t print(event)\\n\\t return {'statusCode': 200, 'headers': {'dance': 'party'}, 'body': json.dumps({'electric': 'boogaloo'}), 'isBase64Encoded': False}", + "filename": "index.py" + } + ], + "type": "zip" + } + }, + "aws_iam_policy_document": { + "test-lambda_assume-policy-document_D538087D": { + "statement": [ + { + "actions": [ + "sts:AssumeRole" + ], + "effect": "Allow", + "principals": [ + { + "identifiers": [ + "lambda.amazonaws.com", + "edgelambda.amazonaws.com" + ], + "type": "Service" + } + ] + } + ], + "version": "2012-10-17" + }, + "test-lambda_execution-policy-document_D94D17B4": { + "statement": [ + { + "actions": [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents", + "logs:DescribeLogStreams" + ], + "effect": "Allow", + "resources": [ + "arn:aws:logs:*:*:*" + ] + } + ], + "version": "2012-10-17" + } + } + }, + "resource": { + "aws_cloudwatch_log_group": { + "test-lambda_log-group_ACC182B5": { + "depends_on": [ + "aws_lambda_function.test-lambda_8915D118" + ], + "name": "/aws/lambda/\${aws_lambda_function.test-lambda_8915D118.function_name}", + "retention_in_days": 14 + } + }, + "aws_iam_policy": { + "test-lambda_execution-policy_19C785A8": { + "name": "test-lambda-ExecutionRolePolicy", + "policy": "\${data.aws_iam_policy_document.test-lambda_execution-policy-document_D94D17B4.json}" + } + }, + "aws_iam_role": { + "test-lambda_execution-role_9D4EE856": { + "assume_role_policy": "\${data.aws_iam_policy_document.test-lambda_assume-policy-document_D538087D.json}", + "name": "test-lambda-ExecutionRole" + } + }, + "aws_iam_role_policy_attachment": { + "test-lambda_execution-role-policy-attachment_8796505D": { + "depends_on": [ + "aws_iam_role.test-lambda_execution-role_9D4EE856", + "aws_iam_policy.test-lambda_execution-policy_19C785A8" + ], + "policy_arn": "\${aws_iam_policy.test-lambda_execution-policy_19C785A8.arn}", + "role": "\${aws_iam_role.test-lambda_execution-role_9D4EE856.name}" + } + }, + "aws_lambda_alias": { + "test-lambda_alias_CCFDFCE9": { + "depends_on": [ + "aws_lambda_function.test-lambda_8915D118" + ], + "function_name": "\${aws_lambda_function.test-lambda_8915D118.function_name}", + "function_version": "\${element(split(\\":\\", aws_lambda_function.test-lambda_8915D118.qualified_arn), 7)}", + "lifecycle": { + "ignore_changes": [ + "function_version" + ] + }, + "name": "DEPLOYED" + } + }, + "aws_lambda_function": { + "test-lambda_8915D118": { + "filename": "\${data.archive_file.test-lambda_lambda-default-file_7A7DBB68.output_path}", + "function_name": "test-lambda-Function", + "handler": "index.handler", + "lifecycle": { + "ignore_changes": [ + "filename", + "source_code_hash" + ] + }, + "memory_size": 128, + "publish": true, + "reserved_concurrent_executions": 10, + "role": "\${aws_iam_role.test-lambda_execution-role_9D4EE856.arn}", + "runtime": "python3.8", + "source_code_hash": "\${data.archive_file.test-lambda_lambda-default-file_7A7DBB68.output_base64sha256}", + "timeout": 5 + } + }, + "aws_s3_bucket": { + "test-lambda_code-bucket_177F316E": { + "bucket": "pocket-test-lambda", + "force_destroy": true + } + }, + "aws_s3_bucket_acl": { + "test-lambda_code-bucket-acl_C73BD0DB": { + "acl": "private", + "bucket": "\${aws_s3_bucket.test-lambda_code-bucket_177F316E.id}", + "depends_on": [ + "aws_s3_bucket_ownership_controls.test-lambda_code-bucket-ownership-controls_6A1865C3" + ] + } + }, + "aws_s3_bucket_ownership_controls": { + "test-lambda_code-bucket-ownership-controls_6A1865C3": { + "bucket": "\${aws_s3_bucket.test-lambda_code-bucket_177F316E.id}", + "rule": { + "object_ownership": "BucketOwnerPreferred" + } + } + }, + "aws_s3_bucket_public_access_block": { + "test-lambda_code-bucket-public-access-block_1E7CE5AE": { + "block_public_acls": true, + "block_public_policy": true, + "bucket": "\${aws_s3_bucket.test-lambda_code-bucket_177F316E.id}" + } + } + } +}" +`; + +exports[`renders a lambda with environment variables 1`] = ` +"{ + "data": { + "archive_file": { + "test-lambda_lambda-default-file_7A7DBB68": { + "output_path": "index.py.zip", + "source": [ + { + "content": "import json\\ndef handler(event, context):\\n\\t print(event)\\n\\t return {'statusCode': 200, 'headers': {'dance': 'party'}, 'body': json.dumps({'electric': 'boogaloo'}), 'isBase64Encoded': False}", + "filename": "index.py" + } + ], + "type": "zip" + } + }, + "aws_iam_policy_document": { + "test-lambda_assume-policy-document_D538087D": { + "statement": [ + { + "actions": [ + "sts:AssumeRole" + ], + "effect": "Allow", + "principals": [ + { + "identifiers": [ + "lambda.amazonaws.com", + "edgelambda.amazonaws.com" + ], + "type": "Service" + } + ] + } + ], + "version": "2012-10-17" + }, + "test-lambda_execution-policy-document_D94D17B4": { + "statement": [ + { + "actions": [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents", + "logs:DescribeLogStreams" + ], + "effect": "Allow", + "resources": [ + "arn:aws:logs:*:*:*" + ] + } + ], + "version": "2012-10-17" + } + } + }, + "resource": { + "aws_cloudwatch_log_group": { + "test-lambda_log-group_ACC182B5": { + "depends_on": [ + "aws_lambda_function.test-lambda_8915D118" + ], + "name": "/aws/lambda/\${aws_lambda_function.test-lambda_8915D118.function_name}", + "retention_in_days": 14 + } + }, + "aws_iam_policy": { + "test-lambda_execution-policy_19C785A8": { + "name": "test-lambda-ExecutionRolePolicy", + "policy": "\${data.aws_iam_policy_document.test-lambda_execution-policy-document_D94D17B4.json}" + } + }, + "aws_iam_role": { + "test-lambda_execution-role_9D4EE856": { + "assume_role_policy": "\${data.aws_iam_policy_document.test-lambda_assume-policy-document_D538087D.json}", + "name": "test-lambda-ExecutionRole" + } + }, + "aws_iam_role_policy_attachment": { + "test-lambda_execution-role-policy-attachment_8796505D": { + "depends_on": [ + "aws_iam_role.test-lambda_execution-role_9D4EE856", + "aws_iam_policy.test-lambda_execution-policy_19C785A8" + ], + "policy_arn": "\${aws_iam_policy.test-lambda_execution-policy_19C785A8.arn}", + "role": "\${aws_iam_role.test-lambda_execution-role_9D4EE856.name}" + } + }, + "aws_lambda_alias": { + "test-lambda_alias_CCFDFCE9": { + "depends_on": [ + "aws_lambda_function.test-lambda_8915D118" + ], + "function_name": "\${aws_lambda_function.test-lambda_8915D118.function_name}", + "function_version": "\${element(split(\\":\\", aws_lambda_function.test-lambda_8915D118.qualified_arn), 7)}", + "lifecycle": { + "ignore_changes": [ + "function_version" + ] + }, + "name": "DEPLOYED" + } + }, + "aws_lambda_function": { + "test-lambda_8915D118": { + "environment": { + "variables": { + "IS": "good", + "my": "var" + } + }, + "filename": "\${data.archive_file.test-lambda_lambda-default-file_7A7DBB68.output_path}", + "function_name": "test-lambda-Function", + "handler": "index.handler", + "lifecycle": { + "ignore_changes": [ + "filename", + "source_code_hash" + ] + }, + "memory_size": 128, + "publish": true, + "reserved_concurrent_executions": -1, + "role": "\${aws_iam_role.test-lambda_execution-role_9D4EE856.arn}", + "runtime": "python3.8", + "source_code_hash": "\${data.archive_file.test-lambda_lambda-default-file_7A7DBB68.output_base64sha256}", + "timeout": 5 + } + }, + "aws_s3_bucket": { + "test-lambda_code-bucket_177F316E": { + "bucket": "pocket-test-lambda", + "force_destroy": true + } + }, + "aws_s3_bucket_acl": { + "test-lambda_code-bucket-acl_C73BD0DB": { + "acl": "private", + "bucket": "\${aws_s3_bucket.test-lambda_code-bucket_177F316E.id}", + "depends_on": [ + "aws_s3_bucket_ownership_controls.test-lambda_code-bucket-ownership-controls_6A1865C3" + ] + } + }, + "aws_s3_bucket_ownership_controls": { + "test-lambda_code-bucket-ownership-controls_6A1865C3": { + "bucket": "\${aws_s3_bucket.test-lambda_code-bucket_177F316E.id}", + "rule": { + "object_ownership": "BucketOwnerPreferred" + } + } + }, + "aws_s3_bucket_public_access_block": { + "test-lambda_code-bucket-public-access-block_1E7CE5AE": { + "block_public_acls": true, + "block_public_policy": true, + "bucket": "\${aws_s3_bucket.test-lambda_code-bucket_177F316E.id}" + } + } + } +}" +`; + +exports[`renders a lambda with execution policy 1`] = ` +"{ + "data": { + "archive_file": { + "test-lambda_lambda-default-file_7A7DBB68": { + "output_path": "index.py.zip", + "source": [ + { + "content": "import json\\ndef handler(event, context):\\n\\t print(event)\\n\\t return {'statusCode': 200, 'headers': {'dance': 'party'}, 'body': json.dumps({'electric': 'boogaloo'}), 'isBase64Encoded': False}", + "filename": "index.py" + } + ], + "type": "zip" + } + }, + "aws_iam_policy_document": { + "test-lambda_assume-policy-document_D538087D": { + "statement": [ + { + "actions": [ + "sts:AssumeRole" + ], + "effect": "Allow", + "principals": [ + { + "identifiers": [ + "lambda.amazonaws.com", + "edgelambda.amazonaws.com" + ], + "type": "Service" + } + ] + } + ], + "version": "2012-10-17" + }, + "test-lambda_execution-policy-document_D94D17B4": { + "statement": [ + { + "actions": [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents", + "logs:DescribeLogStreams" + ], + "effect": "Allow", + "resources": [ + "arn:aws:logs:*:*:*" + ] + }, + { + "actions": [ + "*" + ], + "effect": "Allow", + "resources": [ + "*" + ] + } + ], + "version": "2012-10-17" + } + } + }, + "resource": { + "aws_cloudwatch_log_group": { + "test-lambda_log-group_ACC182B5": { + "depends_on": [ + "aws_lambda_function.test-lambda_8915D118" + ], + "name": "/aws/lambda/\${aws_lambda_function.test-lambda_8915D118.function_name}", + "retention_in_days": 14 + } + }, + "aws_iam_policy": { + "test-lambda_execution-policy_19C785A8": { + "name": "test-lambda-ExecutionRolePolicy", + "policy": "\${data.aws_iam_policy_document.test-lambda_execution-policy-document_D94D17B4.json}" + } + }, + "aws_iam_role": { + "test-lambda_execution-role_9D4EE856": { + "assume_role_policy": "\${data.aws_iam_policy_document.test-lambda_assume-policy-document_D538087D.json}", + "name": "test-lambda-ExecutionRole" + } + }, + "aws_iam_role_policy_attachment": { + "test-lambda_execution-role-policy-attachment_8796505D": { + "depends_on": [ + "aws_iam_role.test-lambda_execution-role_9D4EE856", + "aws_iam_policy.test-lambda_execution-policy_19C785A8" + ], + "policy_arn": "\${aws_iam_policy.test-lambda_execution-policy_19C785A8.arn}", + "role": "\${aws_iam_role.test-lambda_execution-role_9D4EE856.name}" + } + }, + "aws_lambda_alias": { + "test-lambda_alias_CCFDFCE9": { + "depends_on": [ + "aws_lambda_function.test-lambda_8915D118" + ], + "function_name": "\${aws_lambda_function.test-lambda_8915D118.function_name}", + "function_version": "\${element(split(\\":\\", aws_lambda_function.test-lambda_8915D118.qualified_arn), 7)}", + "lifecycle": { + "ignore_changes": [ + "function_version" + ] + }, + "name": "DEPLOYED" + } + }, + "aws_lambda_function": { + "test-lambda_8915D118": { + "filename": "\${data.archive_file.test-lambda_lambda-default-file_7A7DBB68.output_path}", + "function_name": "test-lambda-Function", + "handler": "index.handler", + "lifecycle": { + "ignore_changes": [ + "filename", + "source_code_hash" + ] + }, + "memory_size": 128, + "publish": true, + "reserved_concurrent_executions": -1, + "role": "\${aws_iam_role.test-lambda_execution-role_9D4EE856.arn}", + "runtime": "python3.8", + "source_code_hash": "\${data.archive_file.test-lambda_lambda-default-file_7A7DBB68.output_base64sha256}", + "timeout": 5 + } + }, + "aws_s3_bucket": { + "test-lambda_code-bucket_177F316E": { + "bucket": "pocket-test-lambda", + "force_destroy": true + } + }, + "aws_s3_bucket_acl": { + "test-lambda_code-bucket-acl_C73BD0DB": { + "acl": "private", + "bucket": "\${aws_s3_bucket.test-lambda_code-bucket_177F316E.id}", + "depends_on": [ + "aws_s3_bucket_ownership_controls.test-lambda_code-bucket-ownership-controls_6A1865C3" + ] + } + }, + "aws_s3_bucket_ownership_controls": { + "test-lambda_code-bucket-ownership-controls_6A1865C3": { + "bucket": "\${aws_s3_bucket.test-lambda_code-bucket_177F316E.id}", + "rule": { + "object_ownership": "BucketOwnerPreferred" + } + } + }, + "aws_s3_bucket_public_access_block": { + "test-lambda_code-bucket-public-access-block_1E7CE5AE": { + "block_public_acls": true, + "block_public_policy": true, + "bucket": "\${aws_s3_bucket.test-lambda_code-bucket_177F316E.id}" + } + } + } +}" +`; + +exports[`renders a lambda with lambda description 1`] = ` +"{ + "data": { + "archive_file": { + "test-lambda_lambda-default-file_7A7DBB68": { + "output_path": "index.py.zip", + "source": [ + { + "content": "import json\\ndef handler(event, context):\\n\\t print(event)\\n\\t return {'statusCode': 200, 'headers': {'dance': 'party'}, 'body': json.dumps({'electric': 'boogaloo'}), 'isBase64Encoded': False}", + "filename": "index.py" + } + ], + "type": "zip" + } + }, + "aws_iam_policy_document": { + "test-lambda_assume-policy-document_D538087D": { + "statement": [ + { + "actions": [ + "sts:AssumeRole" + ], + "effect": "Allow", + "principals": [ + { + "identifiers": [ + "lambda.amazonaws.com", + "edgelambda.amazonaws.com" + ], + "type": "Service" + } + ] + } + ], + "version": "2012-10-17" + }, + "test-lambda_execution-policy-document_D94D17B4": { + "statement": [ + { + "actions": [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents", + "logs:DescribeLogStreams" + ], + "effect": "Allow", + "resources": [ + "arn:aws:logs:*:*:*" + ] + } + ], + "version": "2012-10-17" + } + } + }, + "resource": { + "aws_cloudwatch_log_group": { + "test-lambda_log-group_ACC182B5": { + "depends_on": [ + "aws_lambda_function.test-lambda_8915D118" + ], + "name": "/aws/lambda/\${aws_lambda_function.test-lambda_8915D118.function_name}", + "retention_in_days": 14 + } + }, + "aws_iam_policy": { + "test-lambda_execution-policy_19C785A8": { + "name": "test-lambda-ExecutionRolePolicy", + "policy": "\${data.aws_iam_policy_document.test-lambda_execution-policy-document_D94D17B4.json}" + } + }, + "aws_iam_role": { + "test-lambda_execution-role_9D4EE856": { + "assume_role_policy": "\${data.aws_iam_policy_document.test-lambda_assume-policy-document_D538087D.json}", + "name": "test-lambda-ExecutionRole" + } + }, + "aws_iam_role_policy_attachment": { + "test-lambda_execution-role-policy-attachment_8796505D": { + "depends_on": [ + "aws_iam_role.test-lambda_execution-role_9D4EE856", + "aws_iam_policy.test-lambda_execution-policy_19C785A8" + ], + "policy_arn": "\${aws_iam_policy.test-lambda_execution-policy_19C785A8.arn}", + "role": "\${aws_iam_role.test-lambda_execution-role_9D4EE856.name}" + } + }, + "aws_lambda_alias": { + "test-lambda_alias_CCFDFCE9": { + "depends_on": [ + "aws_lambda_function.test-lambda_8915D118" + ], + "function_name": "\${aws_lambda_function.test-lambda_8915D118.function_name}", + "function_version": "\${element(split(\\":\\", aws_lambda_function.test-lambda_8915D118.qualified_arn), 7)}", + "lifecycle": { + "ignore_changes": [ + "function_version" + ] + }, + "name": "DEPLOYED" + } + }, + "aws_lambda_function": { + "test-lambda_8915D118": { + "filename": "\${data.archive_file.test-lambda_lambda-default-file_7A7DBB68.output_path}", + "function_name": "test-lambda-Function", + "handler": "index.handler", + "lifecycle": { + "ignore_changes": [ + "filename", + "source_code_hash" + ] + }, + "memory_size": 128, + "publish": true, + "reserved_concurrent_executions": -1, + "role": "\${aws_iam_role.test-lambda_execution-role_9D4EE856.arn}", + "runtime": "python3.8", + "source_code_hash": "\${data.archive_file.test-lambda_lambda-default-file_7A7DBB68.output_base64sha256}", + "timeout": 5 + } + }, + "aws_s3_bucket": { + "test-lambda_code-bucket_177F316E": { + "bucket": "pocket-test-lambda", + "force_destroy": true + } + }, + "aws_s3_bucket_acl": { + "test-lambda_code-bucket-acl_C73BD0DB": { + "acl": "private", + "bucket": "\${aws_s3_bucket.test-lambda_code-bucket_177F316E.id}", + "depends_on": [ + "aws_s3_bucket_ownership_controls.test-lambda_code-bucket-ownership-controls_6A1865C3" + ] + } + }, + "aws_s3_bucket_ownership_controls": { + "test-lambda_code-bucket-ownership-controls_6A1865C3": { + "bucket": "\${aws_s3_bucket.test-lambda_code-bucket_177F316E.id}", + "rule": { + "object_ownership": "BucketOwnerPreferred" + } + } + }, + "aws_s3_bucket_public_access_block": { + "test-lambda_code-bucket-public-access-block_1E7CE5AE": { + "block_public_acls": true, + "block_public_policy": true, + "bucket": "\${aws_s3_bucket.test-lambda_code-bucket_177F316E.id}" + } + } + } +}" +`; + +exports[`renders a lambda with log retention 1`] = ` +"{ + "data": { + "archive_file": { + "test-lambda_lambda-default-file_7A7DBB68": { + "output_path": "index.py.zip", + "source": [ + { + "content": "import json\\ndef handler(event, context):\\n\\t print(event)\\n\\t return {'statusCode': 200, 'headers': {'dance': 'party'}, 'body': json.dumps({'electric': 'boogaloo'}), 'isBase64Encoded': False}", + "filename": "index.py" + } + ], + "type": "zip" + } + }, + "aws_iam_policy_document": { + "test-lambda_assume-policy-document_D538087D": { + "statement": [ + { + "actions": [ + "sts:AssumeRole" + ], + "effect": "Allow", + "principals": [ + { + "identifiers": [ + "lambda.amazonaws.com", + "edgelambda.amazonaws.com" + ], + "type": "Service" + } + ] + } + ], + "version": "2012-10-17" + }, + "test-lambda_execution-policy-document_D94D17B4": { + "statement": [ + { + "actions": [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents", + "logs:DescribeLogStreams" + ], + "effect": "Allow", + "resources": [ + "arn:aws:logs:*:*:*" + ] + } + ], + "version": "2012-10-17" + } + } + }, + "resource": { + "aws_cloudwatch_log_group": { + "test-lambda_log-group_ACC182B5": { + "depends_on": [ + "aws_lambda_function.test-lambda_8915D118" + ], + "name": "/aws/lambda/\${aws_lambda_function.test-lambda_8915D118.function_name}", + "retention_in_days": 10 + } + }, + "aws_iam_policy": { + "test-lambda_execution-policy_19C785A8": { + "name": "test-lambda-ExecutionRolePolicy", + "policy": "\${data.aws_iam_policy_document.test-lambda_execution-policy-document_D94D17B4.json}" + } + }, + "aws_iam_role": { + "test-lambda_execution-role_9D4EE856": { + "assume_role_policy": "\${data.aws_iam_policy_document.test-lambda_assume-policy-document_D538087D.json}", + "name": "test-lambda-ExecutionRole" + } + }, + "aws_iam_role_policy_attachment": { + "test-lambda_execution-role-policy-attachment_8796505D": { + "depends_on": [ + "aws_iam_role.test-lambda_execution-role_9D4EE856", + "aws_iam_policy.test-lambda_execution-policy_19C785A8" + ], + "policy_arn": "\${aws_iam_policy.test-lambda_execution-policy_19C785A8.arn}", + "role": "\${aws_iam_role.test-lambda_execution-role_9D4EE856.name}" + } + }, + "aws_lambda_alias": { + "test-lambda_alias_CCFDFCE9": { + "depends_on": [ + "aws_lambda_function.test-lambda_8915D118" + ], + "function_name": "\${aws_lambda_function.test-lambda_8915D118.function_name}", + "function_version": "\${element(split(\\":\\", aws_lambda_function.test-lambda_8915D118.qualified_arn), 7)}", + "lifecycle": { + "ignore_changes": [ + "function_version" + ] + }, + "name": "DEPLOYED" + } + }, + "aws_lambda_function": { + "test-lambda_8915D118": { + "filename": "\${data.archive_file.test-lambda_lambda-default-file_7A7DBB68.output_path}", + "function_name": "test-lambda-Function", + "handler": "index.handler", + "lifecycle": { + "ignore_changes": [ + "filename", + "source_code_hash" + ] + }, + "memory_size": 128, + "publish": true, + "reserved_concurrent_executions": -1, + "role": "\${aws_iam_role.test-lambda_execution-role_9D4EE856.arn}", + "runtime": "python3.8", + "source_code_hash": "\${data.archive_file.test-lambda_lambda-default-file_7A7DBB68.output_base64sha256}", + "timeout": 5 + } + }, + "aws_s3_bucket": { + "test-lambda_code-bucket_177F316E": { + "bucket": "pocket-test-lambda", + "force_destroy": true + } + }, + "aws_s3_bucket_acl": { + "test-lambda_code-bucket-acl_C73BD0DB": { + "acl": "private", + "bucket": "\${aws_s3_bucket.test-lambda_code-bucket_177F316E.id}", + "depends_on": [ + "aws_s3_bucket_ownership_controls.test-lambda_code-bucket-ownership-controls_6A1865C3" + ] + } + }, + "aws_s3_bucket_ownership_controls": { + "test-lambda_code-bucket-ownership-controls_6A1865C3": { + "bucket": "\${aws_s3_bucket.test-lambda_code-bucket_177F316E.id}", + "rule": { + "object_ownership": "BucketOwnerPreferred" + } + } + }, + "aws_s3_bucket_public_access_block": { + "test-lambda_code-bucket-public-access-block_1E7CE5AE": { + "block_public_acls": true, + "block_public_policy": true, + "bucket": "\${aws_s3_bucket.test-lambda_code-bucket_177F316E.id}" + } + } + } +}" +`; + +exports[`renders a lambda with memorySizeInMb 1`] = ` +"{ + "data": { + "archive_file": { + "test-lambda_lambda-default-file_7A7DBB68": { + "output_path": "index.py.zip", + "source": [ + { + "content": "import json\\ndef handler(event, context):\\n\\t print(event)\\n\\t return {'statusCode': 200, 'headers': {'dance': 'party'}, 'body': json.dumps({'electric': 'boogaloo'}), 'isBase64Encoded': False}", + "filename": "index.py" + } + ], + "type": "zip" + } + }, + "aws_iam_policy_document": { + "test-lambda_assume-policy-document_D538087D": { + "statement": [ + { + "actions": [ + "sts:AssumeRole" + ], + "effect": "Allow", + "principals": [ + { + "identifiers": [ + "lambda.amazonaws.com", + "edgelambda.amazonaws.com" + ], + "type": "Service" + } + ] + } + ], + "version": "2012-10-17" + }, + "test-lambda_execution-policy-document_D94D17B4": { + "statement": [ + { + "actions": [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents", + "logs:DescribeLogStreams" + ], + "effect": "Allow", + "resources": [ + "arn:aws:logs:*:*:*" + ] + } + ], + "version": "2012-10-17" + } + } + }, + "resource": { + "aws_cloudwatch_log_group": { + "test-lambda_log-group_ACC182B5": { + "depends_on": [ + "aws_lambda_function.test-lambda_8915D118" + ], + "name": "/aws/lambda/\${aws_lambda_function.test-lambda_8915D118.function_name}", + "retention_in_days": 14 + } + }, + "aws_iam_policy": { + "test-lambda_execution-policy_19C785A8": { + "name": "test-lambda-ExecutionRolePolicy", + "policy": "\${data.aws_iam_policy_document.test-lambda_execution-policy-document_D94D17B4.json}" + } + }, + "aws_iam_role": { + "test-lambda_execution-role_9D4EE856": { + "assume_role_policy": "\${data.aws_iam_policy_document.test-lambda_assume-policy-document_D538087D.json}", + "name": "test-lambda-ExecutionRole" + } + }, + "aws_iam_role_policy_attachment": { + "test-lambda_execution-role-policy-attachment_8796505D": { + "depends_on": [ + "aws_iam_role.test-lambda_execution-role_9D4EE856", + "aws_iam_policy.test-lambda_execution-policy_19C785A8" + ], + "policy_arn": "\${aws_iam_policy.test-lambda_execution-policy_19C785A8.arn}", + "role": "\${aws_iam_role.test-lambda_execution-role_9D4EE856.name}" + } + }, + "aws_lambda_alias": { + "test-lambda_alias_CCFDFCE9": { + "depends_on": [ + "aws_lambda_function.test-lambda_8915D118" + ], + "function_name": "\${aws_lambda_function.test-lambda_8915D118.function_name}", + "function_version": "\${element(split(\\":\\", aws_lambda_function.test-lambda_8915D118.qualified_arn), 7)}", + "lifecycle": { + "ignore_changes": [ + "function_version" + ] + }, + "name": "DEPLOYED" + } + }, + "aws_lambda_function": { + "test-lambda_8915D118": { + "filename": "\${data.archive_file.test-lambda_lambda-default-file_7A7DBB68.output_path}", + "function_name": "test-lambda-Function", + "handler": "index.handler", + "lifecycle": { + "ignore_changes": [ + "filename", + "source_code_hash" + ] + }, + "memory_size": 512, + "publish": true, + "reserved_concurrent_executions": -1, + "role": "\${aws_iam_role.test-lambda_execution-role_9D4EE856.arn}", + "runtime": "python3.8", + "source_code_hash": "\${data.archive_file.test-lambda_lambda-default-file_7A7DBB68.output_base64sha256}", + "timeout": 5 + } + }, + "aws_s3_bucket": { + "test-lambda_code-bucket_177F316E": { + "bucket": "pocket-test-lambda", + "force_destroy": true + } + }, + "aws_s3_bucket_acl": { + "test-lambda_code-bucket-acl_C73BD0DB": { + "acl": "private", + "bucket": "\${aws_s3_bucket.test-lambda_code-bucket_177F316E.id}", + "depends_on": [ + "aws_s3_bucket_ownership_controls.test-lambda_code-bucket-ownership-controls_6A1865C3" + ] + } + }, + "aws_s3_bucket_ownership_controls": { + "test-lambda_code-bucket-ownership-controls_6A1865C3": { + "bucket": "\${aws_s3_bucket.test-lambda_code-bucket_177F316E.id}", + "rule": { + "object_ownership": "BucketOwnerPreferred" + } + } + }, + "aws_s3_bucket_public_access_block": { + "test-lambda_code-bucket-public-access-block_1E7CE5AE": { + "block_public_acls": true, + "block_public_policy": true, + "bucket": "\${aws_s3_bucket.test-lambda_code-bucket_177F316E.id}" + } + } + } +}" +`; + +exports[`renders a lambda with s3 bucket 1`] = ` +"{ + "data": { + "archive_file": { + "test-lambda_lambda-default-file_7A7DBB68": { + "output_path": "index.py.zip", + "source": [ + { + "content": "import json\\ndef handler(event, context):\\n\\t print(event)\\n\\t return {'statusCode': 200, 'headers': {'dance': 'party'}, 'body': json.dumps({'electric': 'boogaloo'}), 'isBase64Encoded': False}", + "filename": "index.py" + } + ], + "type": "zip" + } + }, + "aws_iam_policy_document": { + "test-lambda_assume-policy-document_D538087D": { + "statement": [ + { + "actions": [ + "sts:AssumeRole" + ], + "effect": "Allow", + "principals": [ + { + "identifiers": [ + "lambda.amazonaws.com", + "edgelambda.amazonaws.com" + ], + "type": "Service" + } + ] + } + ], + "version": "2012-10-17" + }, + "test-lambda_execution-policy-document_D94D17B4": { + "statement": [ + { + "actions": [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents", + "logs:DescribeLogStreams" + ], + "effect": "Allow", + "resources": [ + "arn:aws:logs:*:*:*" + ] + } + ], + "version": "2012-10-17" + } + } + }, + "resource": { + "aws_cloudwatch_log_group": { + "test-lambda_log-group_ACC182B5": { + "depends_on": [ + "aws_lambda_function.test-lambda_8915D118" + ], + "name": "/aws/lambda/\${aws_lambda_function.test-lambda_8915D118.function_name}", + "retention_in_days": 14 + } + }, + "aws_iam_policy": { + "test-lambda_execution-policy_19C785A8": { + "name": "test-lambda-ExecutionRolePolicy", + "policy": "\${data.aws_iam_policy_document.test-lambda_execution-policy-document_D94D17B4.json}" + } + }, + "aws_iam_role": { + "test-lambda_execution-role_9D4EE856": { + "assume_role_policy": "\${data.aws_iam_policy_document.test-lambda_assume-policy-document_D538087D.json}", + "name": "test-lambda-ExecutionRole" + } + }, + "aws_iam_role_policy_attachment": { + "test-lambda_execution-role-policy-attachment_8796505D": { + "depends_on": [ + "aws_iam_role.test-lambda_execution-role_9D4EE856", + "aws_iam_policy.test-lambda_execution-policy_19C785A8" + ], + "policy_arn": "\${aws_iam_policy.test-lambda_execution-policy_19C785A8.arn}", + "role": "\${aws_iam_role.test-lambda_execution-role_9D4EE856.name}" + } + }, + "aws_lambda_alias": { + "test-lambda_alias_CCFDFCE9": { + "depends_on": [ + "aws_lambda_function.test-lambda_8915D118" + ], + "function_name": "\${aws_lambda_function.test-lambda_8915D118.function_name}", + "function_version": "\${element(split(\\":\\", aws_lambda_function.test-lambda_8915D118.qualified_arn), 7)}", + "lifecycle": { + "ignore_changes": [ + "function_version" + ] + }, + "name": "DEPLOYED" + } + }, + "aws_lambda_function": { + "test-lambda_8915D118": { + "filename": "\${data.archive_file.test-lambda_lambda-default-file_7A7DBB68.output_path}", + "function_name": "test-lambda-Function", + "handler": "index.handler", + "lifecycle": { + "ignore_changes": [ + "filename", + "source_code_hash" + ] + }, + "memory_size": 128, + "publish": true, + "reserved_concurrent_executions": -1, + "role": "\${aws_iam_role.test-lambda_execution-role_9D4EE856.arn}", + "runtime": "python3.8", + "source_code_hash": "\${data.archive_file.test-lambda_lambda-default-file_7A7DBB68.output_base64sha256}", + "timeout": 5 + } + }, + "aws_s3_bucket": { + "test-lambda_code-bucket_177F316E": { + "bucket": "test-bucket", + "force_destroy": true + } + }, + "aws_s3_bucket_acl": { + "test-lambda_code-bucket-acl_C73BD0DB": { + "acl": "private", + "bucket": "\${aws_s3_bucket.test-lambda_code-bucket_177F316E.id}", + "depends_on": [ + "aws_s3_bucket_ownership_controls.test-lambda_code-bucket-ownership-controls_6A1865C3" + ] + } + }, + "aws_s3_bucket_ownership_controls": { + "test-lambda_code-bucket-ownership-controls_6A1865C3": { + "bucket": "\${aws_s3_bucket.test-lambda_code-bucket_177F316E.id}", + "rule": { + "object_ownership": "BucketOwnerPreferred" + } + } + }, + "aws_s3_bucket_public_access_block": { + "test-lambda_code-bucket-public-access-block_1E7CE5AE": { + "block_public_acls": true, + "block_public_policy": true, + "bucket": "\${aws_s3_bucket.test-lambda_code-bucket_177F316E.id}" + } + } + } +}" +`; + +exports[`renders a lambda with timeout 1`] = ` +"{ + "data": { + "archive_file": { + "test-lambda_lambda-default-file_7A7DBB68": { + "output_path": "index.py.zip", + "source": [ + { + "content": "import json\\ndef handler(event, context):\\n\\t print(event)\\n\\t return {'statusCode': 200, 'headers': {'dance': 'party'}, 'body': json.dumps({'electric': 'boogaloo'}), 'isBase64Encoded': False}", + "filename": "index.py" + } + ], + "type": "zip" + } + }, + "aws_iam_policy_document": { + "test-lambda_assume-policy-document_D538087D": { + "statement": [ + { + "actions": [ + "sts:AssumeRole" + ], + "effect": "Allow", + "principals": [ + { + "identifiers": [ + "lambda.amazonaws.com", + "edgelambda.amazonaws.com" + ], + "type": "Service" + } + ] + } + ], + "version": "2012-10-17" + }, + "test-lambda_execution-policy-document_D94D17B4": { + "statement": [ + { + "actions": [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents", + "logs:DescribeLogStreams" + ], + "effect": "Allow", + "resources": [ + "arn:aws:logs:*:*:*" + ] + } + ], + "version": "2012-10-17" + } + } + }, + "resource": { + "aws_cloudwatch_log_group": { + "test-lambda_log-group_ACC182B5": { + "depends_on": [ + "aws_lambda_function.test-lambda_8915D118" + ], + "name": "/aws/lambda/\${aws_lambda_function.test-lambda_8915D118.function_name}", + "retention_in_days": 14 + } + }, + "aws_iam_policy": { + "test-lambda_execution-policy_19C785A8": { + "name": "test-lambda-ExecutionRolePolicy", + "policy": "\${data.aws_iam_policy_document.test-lambda_execution-policy-document_D94D17B4.json}" + } + }, + "aws_iam_role": { + "test-lambda_execution-role_9D4EE856": { + "assume_role_policy": "\${data.aws_iam_policy_document.test-lambda_assume-policy-document_D538087D.json}", + "name": "test-lambda-ExecutionRole" + } + }, + "aws_iam_role_policy_attachment": { + "test-lambda_execution-role-policy-attachment_8796505D": { + "depends_on": [ + "aws_iam_role.test-lambda_execution-role_9D4EE856", + "aws_iam_policy.test-lambda_execution-policy_19C785A8" + ], + "policy_arn": "\${aws_iam_policy.test-lambda_execution-policy_19C785A8.arn}", + "role": "\${aws_iam_role.test-lambda_execution-role_9D4EE856.name}" + } + }, + "aws_lambda_alias": { + "test-lambda_alias_CCFDFCE9": { + "depends_on": [ + "aws_lambda_function.test-lambda_8915D118" + ], + "function_name": "\${aws_lambda_function.test-lambda_8915D118.function_name}", + "function_version": "\${element(split(\\":\\", aws_lambda_function.test-lambda_8915D118.qualified_arn), 7)}", + "lifecycle": { + "ignore_changes": [ + "function_version" + ] + }, + "name": "DEPLOYED" + } + }, + "aws_lambda_function": { + "test-lambda_8915D118": { + "filename": "\${data.archive_file.test-lambda_lambda-default-file_7A7DBB68.output_path}", + "function_name": "test-lambda-Function", + "handler": "index.handler", + "lifecycle": { + "ignore_changes": [ + "filename", + "source_code_hash" + ] + }, + "memory_size": 128, + "publish": true, + "reserved_concurrent_executions": -1, + "role": "\${aws_iam_role.test-lambda_execution-role_9D4EE856.arn}", + "runtime": "python3.8", + "source_code_hash": "\${data.archive_file.test-lambda_lambda-default-file_7A7DBB68.output_base64sha256}", + "timeout": 300 + } + }, + "aws_s3_bucket": { + "test-lambda_code-bucket_177F316E": { + "bucket": "pocket-test-lambda", + "force_destroy": true + } + }, + "aws_s3_bucket_acl": { + "test-lambda_code-bucket-acl_C73BD0DB": { + "acl": "private", + "bucket": "\${aws_s3_bucket.test-lambda_code-bucket_177F316E.id}", + "depends_on": [ + "aws_s3_bucket_ownership_controls.test-lambda_code-bucket-ownership-controls_6A1865C3" + ] + } + }, + "aws_s3_bucket_ownership_controls": { + "test-lambda_code-bucket-ownership-controls_6A1865C3": { + "bucket": "\${aws_s3_bucket.test-lambda_code-bucket_177F316E.id}", + "rule": { + "object_ownership": "BucketOwnerPreferred" + } + } + }, + "aws_s3_bucket_public_access_block": { + "test-lambda_code-bucket-public-access-block_1E7CE5AE": { + "block_public_acls": true, + "block_public_policy": true, + "bucket": "\${aws_s3_bucket.test-lambda_code-bucket_177F316E.id}" + } + } + } +}" +`; + +exports[`renders a lambda with vpc config 1`] = ` +"{ + "data": { + "archive_file": { + "test-lambda_lambda-default-file_7A7DBB68": { + "output_path": "index.py.zip", + "source": [ + { + "content": "import json\\ndef handler(event, context):\\n\\t print(event)\\n\\t return {'statusCode': 200, 'headers': {'dance': 'party'}, 'body': json.dumps({'electric': 'boogaloo'}), 'isBase64Encoded': False}", + "filename": "index.py" + } + ], + "type": "zip" + } + }, + "aws_iam_policy_document": { + "test-lambda_assume-policy-document_D538087D": { + "statement": [ + { + "actions": [ + "sts:AssumeRole" + ], + "effect": "Allow", + "principals": [ + { + "identifiers": [ + "lambda.amazonaws.com", + "edgelambda.amazonaws.com" + ], + "type": "Service" + } + ] + } + ], + "version": "2012-10-17" + }, + "test-lambda_execution-policy-document_D94D17B4": { + "statement": [ + { + "actions": [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents", + "logs:DescribeLogStreams" + ], + "effect": "Allow", + "resources": [ + "arn:aws:logs:*:*:*" + ] + }, + { + "actions": [ + "ec2:DescribeNetworkInterfaces", + "ec2:CreateNetworkInterface", + "ec2:DeleteNetworkInterface", + "ec2:DescribeInstances", + "ec2:AttachNetworkInterface" + ], + "effect": "Allow", + "resources": [ + "*" + ] + } + ], + "version": "2012-10-17" + } + } + }, + "resource": { + "aws_cloudwatch_log_group": { + "test-lambda_log-group_ACC182B5": { + "depends_on": [ + "aws_lambda_function.test-lambda_8915D118" + ], + "name": "/aws/lambda/\${aws_lambda_function.test-lambda_8915D118.function_name}", + "retention_in_days": 14 + } + }, + "aws_iam_policy": { + "test-lambda_execution-policy_19C785A8": { + "name": "test-lambda-ExecutionRolePolicy", + "policy": "\${data.aws_iam_policy_document.test-lambda_execution-policy-document_D94D17B4.json}" + } + }, + "aws_iam_role": { + "test-lambda_execution-role_9D4EE856": { + "assume_role_policy": "\${data.aws_iam_policy_document.test-lambda_assume-policy-document_D538087D.json}", + "name": "test-lambda-ExecutionRole" + } + }, + "aws_iam_role_policy_attachment": { + "test-lambda_execution-role-policy-attachment_8796505D": { + "depends_on": [ + "aws_iam_role.test-lambda_execution-role_9D4EE856", + "aws_iam_policy.test-lambda_execution-policy_19C785A8" + ], + "policy_arn": "\${aws_iam_policy.test-lambda_execution-policy_19C785A8.arn}", + "role": "\${aws_iam_role.test-lambda_execution-role_9D4EE856.name}" + } + }, + "aws_lambda_alias": { + "test-lambda_alias_CCFDFCE9": { + "depends_on": [ + "aws_lambda_function.test-lambda_8915D118" + ], + "function_name": "\${aws_lambda_function.test-lambda_8915D118.function_name}", + "function_version": "\${element(split(\\":\\", aws_lambda_function.test-lambda_8915D118.qualified_arn), 7)}", + "lifecycle": { + "ignore_changes": [ + "function_version" + ] + }, + "name": "DEPLOYED" + } + }, + "aws_lambda_function": { + "test-lambda_8915D118": { + "filename": "\${data.archive_file.test-lambda_lambda-default-file_7A7DBB68.output_path}", + "function_name": "test-lambda-Function", + "handler": "index.handler", + "lifecycle": { + "ignore_changes": [ + "filename", + "source_code_hash" + ] + }, + "memory_size": 128, + "publish": true, + "reserved_concurrent_executions": -1, + "role": "\${aws_iam_role.test-lambda_execution-role_9D4EE856.arn}", + "runtime": "python3.8", + "source_code_hash": "\${data.archive_file.test-lambda_lambda-default-file_7A7DBB68.output_base64sha256}", + "timeout": 5, + "vpc_config": { + "security_group_ids": [ + "sec1", + "sec2" + ], + "subnet_ids": [ + "1", + "2" + ] + } + } + }, + "aws_s3_bucket": { + "test-lambda_code-bucket_177F316E": { + "bucket": "pocket-test-lambda", + "force_destroy": true + } + }, + "aws_s3_bucket_acl": { + "test-lambda_code-bucket-acl_C73BD0DB": { + "acl": "private", + "bucket": "\${aws_s3_bucket.test-lambda_code-bucket_177F316E.id}", + "depends_on": [ + "aws_s3_bucket_ownership_controls.test-lambda_code-bucket-ownership-controls_6A1865C3" + ] + } + }, + "aws_s3_bucket_ownership_controls": { + "test-lambda_code-bucket-ownership-controls_6A1865C3": { + "bucket": "\${aws_s3_bucket.test-lambda_code-bucket_177F316E.id}", + "rule": { + "object_ownership": "BucketOwnerPreferred" + } + } + }, + "aws_s3_bucket_public_access_block": { + "test-lambda_code-bucket-public-access-block_1E7CE5AE": { + "block_public_acls": true, + "block_public_policy": true, + "bucket": "\${aws_s3_bucket.test-lambda_code-bucket_177F316E.id}" + } + } + } +}" +`; diff --git a/packages/terraform-modules/src/testHelpers.ts b/packages/terraform-modules/src/testHelpers.ts new file mode 100644 index 00000000..da8c247b --- /dev/null +++ b/packages/terraform-modules/src/testHelpers.ts @@ -0,0 +1,10 @@ +import { Resource, TerraformStack } from 'cdktf'; +import { Construct } from 'constructs'; + +// used to test individual static functions within component classes +// generates a basic/empty construct Resource to provide context +export class TestResource extends Resource { + constructor(scope: TerraformStack | Construct, name: string) { + super(scope, name); + } +} diff --git a/packages/terraform-modules/src/utilities.jest.ts b/packages/terraform-modules/src/utilities.jest.ts new file mode 100644 index 00000000..9e1fccd2 --- /dev/null +++ b/packages/terraform-modules/src/utilities.jest.ts @@ -0,0 +1,31 @@ +import { getRootDomain, truncateString } from './utilities'; + +describe('utilities', () => { + describe('getRootDomain()', () => { + it('gets root domain when root is the domain', () => { + expect(getRootDomain('getpocket.com')).toBe('getpocket.com'); + }); + + it('gets root domain when root has a subdomain', () => { + expect(getRootDomain('feature.getpocket.com')).toBe('getpocket.com'); + }); + + it('gets root domain when root has multiple subdomains', () => { + expect(getRootDomain('test.feature.getpocket.com')).toBe('getpocket.com'); + }); + + it('gets root domain when domain is www', () => { + expect(getRootDomain('www.getpocket.com')).toBe('getpocket.com'); + }); + }); + + describe('truncateString()', () => { + it('truncates more then 6', () => { + expect(truncateString('getpocket.com', 6)).toBe('getpoc'); + }); + + it('ignores less then 6', () => { + expect(truncateString('get', 6)).toBe('get'); + }); + }); +}); diff --git a/packages/terraform-modules/src/utilities.ts b/packages/terraform-modules/src/utilities.ts new file mode 100644 index 00000000..3a23ebfd --- /dev/null +++ b/packages/terraform-modules/src/utilities.ts @@ -0,0 +1,25 @@ +import { parseDomain, ParseResultListed, ParseResultType } from 'parse-domain'; + +/** + * Takes a domain like test.getpocket.com and returns the root level domain (getpocket.com) + * @param inputDomain + */ +export const getRootDomain = (inputDomain: string): string => { + const parseResult = parseDomain(inputDomain); + + if (parseResult.type == ParseResultType.Invalid) { + throw new Error('Invalid domain'); + } + const { domain, topLevelDomains } = parseResult as ParseResultListed; + return `${domain}.${topLevelDomains.join('.')}`; +}; + +export const truncateString = (str: string, num: number): string => { + // If the length of str is less than or equal to num + // just return str--don't truncate it. + if (str.length <= num) { + return str; + } + // Return str truncated + return str.slice(0, num); +}; diff --git a/packages/terraform-modules/tsconfig.json b/packages/terraform-modules/tsconfig.json new file mode 100644 index 00000000..a533795e --- /dev/null +++ b/packages/terraform-modules/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "tsconfig/library.json", + "compilerOptions": { + "outDir": "dist", + "rootDir": "src" + }, + "exclude": ["node_modules/", "dist/"], + "include": ["src/**/*.ts", "src/config"] +} diff --git a/packages/tsconfig/cdktf.json b/packages/tsconfig/cdktf.json new file mode 100644 index 00000000..c8d82264 --- /dev/null +++ b/packages/tsconfig/cdktf.json @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "experimentalDecorators": true, + "emitDecoratorMetadata": true, + "target": "es2019", + "module": "commonjs", + "lib": [ + "dom", + "es2019", + "es2020.bigint", + "es2020.string", + "es2020.symbol.wellknown" + ], + "removeComments": true, + "esModuleInterop": true, + "noEmitOnError": false, + "sourceMap": true, + "inlineSources": true, + "sourceRoot": "/", + "declaration": true + } +} \ No newline at end of file diff --git a/packages/tsconfig/lambda.json b/packages/tsconfig/lambda.json new file mode 100644 index 00000000..b160371e --- /dev/null +++ b/packages/tsconfig/lambda.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "target": "es2017", + "module": "commonjs", + "outDir": "dist", + "sourceMap": true, + "esModuleInterop": true, + "noEmitHelpers": true, + "importHelpers": true, + "removeComments": true + } +} \ No newline at end of file diff --git a/packages/tsconfig/library.json b/packages/tsconfig/library.json new file mode 100644 index 00000000..c8d82264 --- /dev/null +++ b/packages/tsconfig/library.json @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "experimentalDecorators": true, + "emitDecoratorMetadata": true, + "target": "es2019", + "module": "commonjs", + "lib": [ + "dom", + "es2019", + "es2020.bigint", + "es2020.string", + "es2020.symbol.wellknown" + ], + "removeComments": true, + "esModuleInterop": true, + "noEmitOnError": false, + "sourceMap": true, + "inlineSources": true, + "sourceRoot": "/", + "declaration": true + } +} \ No newline at end of file diff --git a/packages/tsconfig/package.json b/packages/tsconfig/package.json index 6496446b..5924bbb0 100644 --- a/packages/tsconfig/package.json +++ b/packages/tsconfig/package.json @@ -1,3 +1,20 @@ { - "name": "tsconfig" -} \ No newline at end of file + "name": "tsconfig", + "version": "0.0.0", + "private": true, + "license": "MIT", + "type": "module", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@types/node": "^20.10", + "ts-node": "10.9.2", + "tslib": "2.6.2", + "typescript": "5.3.3", + "tsup": "8.0.1" + }, + "engines": { + "node": "^20.10" + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 32184bd2..8c019814 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -14,7 +14,72 @@ importers: devDependencies: turbo: specifier: latest - version: 1.11.2 + version: 1.11.3 + + infrastructure/prospect-api: + dependencies: + '@pocket-tools/terraform-modules': + specifier: workspace:* + version: link:../../packages/terraform-modules + devDependencies: + eslint-config-custom: + specifier: workspace:* + version: link:../../packages/eslint-config-custom + tsconfig: + specifier: workspace:* + version: link:../../packages/tsconfig + + lambdas/prospect-api-lambda: + dependencies: + '@aws-sdk/client-dynamodb': + specifier: 3.391.0 + version: 3.391.0 + '@aws-sdk/client-lambda': + specifier: 3.391.0 + version: 3.391.0 + '@aws-sdk/lib-dynamodb': + specifier: 3.391.0 + version: 3.391.0(@aws-sdk/client-dynamodb@3.391.0) + '@sentry/serverless': + specifier: 6.19.7 + version: 6.19.7 + prospectapi-common: + specifier: workspace:* + version: link:../../packages/prospectapi-common + uuid: + specifier: 8.3.2 + version: 8.3.2 + devDependencies: + '@types/aws-lambda': + specifier: 8.10.119 + version: 8.10.119 + '@types/chai': + specifier: 4.3.4 + version: 4.3.4 + '@types/chai-as-promised': + specifier: 7.1.5 + version: 7.1.5 + '@types/jest': + specifier: 28.1.7 + version: 28.1.7 + chai: + specifier: 4.3.7 + version: 4.3.7 + chai-as-promised: + specifier: 7.1.1 + version: 7.1.1(chai@4.3.7) + eslint-config-custom: + specifier: workspace:* + version: link:../../packages/eslint-config-custom + jest: + specifier: 29.7.0 + version: 29.7.0(@types/node@20.10.8)(ts-node@10.9.2) + ts-jest: + specifier: 29.1.1 + version: 29.1.1(@babel/core@7.23.7)(jest@29.7.0)(typescript@5.3.3) + tsconfig: + specifier: workspace:* + version: link:../../packages/tsconfig packages/eslint-config-custom: devDependencies: @@ -43,14 +108,200 @@ importers: specifier: ^5.2.2 version: 5.3.3 - packages/tsconfig: {} + packages/prospectapi-common: + dependencies: + '@apollo/client': + specifier: 3.7.16 + version: 3.7.16(graphql@16.8.1) + '@aws-sdk/client-dynamodb': + specifier: 3.391.0 + version: 3.391.0 + '@aws-sdk/lib-dynamodb': + specifier: 3.391.0 + version: 3.391.0(@aws-sdk/client-dynamodb@3.391.0) + '@faker-js/faker': + specifier: 6.0.0 + version: 6.0.0 + '@sentry/node': + specifier: 6.19.7 + version: 6.19.7 + cross-fetch: + specifier: 3.1.5 + version: 3.1.5 + graphql-tag: + specifier: 2.12.6 + version: 2.12.6(graphql@16.8.1) + tldts: + specifier: 5.7.87 + version: 5.7.87 + devDependencies: + '@types/chai': + specifier: 4.3.4 + version: 4.3.4 + '@types/jest': + specifier: 28.1.7 + version: 28.1.7 + chai: + specifier: 4.3.7 + version: 4.3.7 + eslint-config-custom: + specifier: workspace:* + version: link:../eslint-config-custom + jest: + specifier: 28.1.3 + version: 28.1.3 + ts-jest: + specifier: 28.0.8 + version: 28.0.8(@babel/core@7.23.7)(jest@28.1.3)(typescript@5.3.3) + tsconfig: + specifier: workspace:* + version: link:../tsconfig + + packages/terraform-modules: + dependencies: + '@cdktf/provider-archive': + specifier: 9.0.1 + version: 9.0.1(cdktf@0.20.0-pre.70)(constructs@10.3.0) + '@cdktf/provider-aws': + specifier: 18.2.0 + version: 18.2.0(cdktf@0.20.0-pre.70)(constructs@10.3.0) + '@cdktf/provider-local': + specifier: 9.0.1 + version: 9.0.1(cdktf@0.20.0-pre.70)(constructs@10.3.0) + '@cdktf/provider-newrelic': + specifier: 11.0.5 + version: 11.0.5(cdktf@0.20.0-pre.70)(constructs@10.3.0) + '@cdktf/provider-null': + specifier: 9.0.1 + version: 9.0.1(cdktf@0.20.0-pre.70)(constructs@10.3.0) + '@cdktf/provider-pagerduty': + specifier: 12.2.0 + version: 12.2.0(cdktf@0.20.0-pre.70)(constructs@10.3.0) + '@cdktf/provider-time': + specifier: 9.0.2 + version: 9.0.2(cdktf@0.20.0-pre.70)(constructs@10.3.0) + cdktf: + specifier: 0.20.0-pre.70 + version: 0.20.0-pre.70(constructs@10.3.0) + cdktf-cli: + specifier: 0.20.0-pre.70 + version: 0.20.0-pre.70(ink@3.2.0)(react@17.0.2) + constructs: + specifier: 10.3.0 + version: 10.3.0 + parse-domain: + specifier: 5.0.0 + version: 5.0.0 + devDependencies: + '@types/jest': + specifier: 29.5.11 + version: 29.5.11 + eslint-config-custom: + specifier: workspace:* + version: link:../eslint-config-custom + jest: + specifier: 29.7.0 + version: 29.7.0(@types/node@20.10.8)(ts-node@10.9.2) + ts-jest: + specifier: 29.1.1 + version: 29.1.1(@babel/core@7.23.7)(jest@29.7.0)(typescript@5.3.3) + tsconfig: + specifier: workspace:* + version: link:../tsconfig + + packages/tsconfig: + dependencies: + '@types/node': + specifier: ^20.10 + version: 20.10.8 + ts-node: + specifier: 10.9.2 + version: 10.9.2(@types/node@20.10.8)(typescript@5.3.3) + tslib: + specifier: 2.6.2 + version: 2.6.2 + tsup: + specifier: 8.0.1 + version: 8.0.1(ts-node@10.9.2)(typescript@5.3.3) + typescript: + specifier: 5.3.3 + version: 5.3.3 + + servers/prospect-api: + dependencies: + '@aws-sdk/client-dynamodb': + specifier: 3.391.0 + version: 3.391.0 + '@aws-sdk/client-eventbridge': + specifier: 3.342.0 + version: 3.342.0 + '@aws-sdk/lib-dynamodb': + specifier: 3.391.0 + version: 3.391.0(@aws-sdk/client-dynamodb@3.391.0) + '@pocket-tools/apollo-utils': + specifier: 3.0.0 + version: 3.0.0 + '@pocket-tools/ts-logger': + specifier: ^1.2.4 + version: 1.3.0(@babel/core@7.23.7)(@types/node@20.10.8)(typescript@5.3.3) + '@snowplow/node-tracker': + specifier: ^3.5.0 + version: 3.19.0 + prospectapi-common: + specifier: workspace:* + version: link:../../packages/prospectapi-common + supertest: + specifier: ^6.3.3 + version: 6.3.3 + devDependencies: + '@faker-js/faker': + specifier: 6.0.0 + version: 6.0.0 + '@types/chai': + specifier: 4.3.4 + version: 4.3.4 + '@types/chai-spies': + specifier: 1.0.3 + version: 1.0.3 + '@types/jest': + specifier: 28.1.7 + version: 28.1.7 + '@types/lodash': + specifier: 4.14.182 + version: 4.14.182 + chai: + specifier: 4.3.7 + version: 4.3.7 + chai-spies: + specifier: 1.0.0 + version: 1.0.0(chai@4.3.7) + eslint-config-custom: + specifier: workspace:* + version: link:../../packages/eslint-config-custom + jest: + specifier: 29.7.0 + version: 29.7.0(@types/node@20.10.8)(ts-node@10.9.2) + lodash: + specifier: 4.17.21 + version: 4.17.21 + nodemon: + specifier: 2.0.22 + version: 2.0.22 + sinon: + specifier: ^17.0.1 + version: 17.0.1 + ts-jest: + specifier: 29.1.1 + version: 29.1.1(@babel/core@7.23.7)(jest@29.7.0)(typescript@5.3.3) + tsconfig: + specifier: workspace:* + version: link:../../packages/tsconfig packages: /@aashutoshrathi/word-wrap@1.2.6: resolution: {integrity: sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==} engines: {node: '>=0.10.0'} - dev: true /@ampproject/remapping@2.2.1: resolution: {integrity: sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==} @@ -58,2179 +309,9821 @@ packages: dependencies: '@jridgewell/gen-mapping': 0.3.3 '@jridgewell/trace-mapping': 0.3.20 - dev: true - /@babel/code-frame@7.23.5: - resolution: {integrity: sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA==} - engines: {node: '>=6.9.0'} + /@apollo/cache-control-types@1.0.3(graphql@16.8.1): + resolution: {integrity: sha512-F17/vCp7QVwom9eG7ToauIKdAxpSoadsJnqIfyryLFSkLSOEqu+eC5Z3N8OXcUVStuOMcNHlyraRsA6rRICu4g==} + peerDependencies: + graphql: 14.x || 15.x || 16.x dependencies: - '@babel/highlight': 7.23.4 - chalk: 2.4.2 - dev: true + graphql: 16.8.1 + dev: false - /@babel/compat-data@7.23.5: - resolution: {integrity: sha512-uU27kfDRlhfKl+w1U6vp16IuvSLtjAxdArVXPa9BvLkrr7CYIsxH5adpHObeAGY/41+syctUWOZ140a2Rvkgjw==} - engines: {node: '>=6.9.0'} - dev: true + /@apollo/client@3.7.16(graphql@16.8.1): + resolution: {integrity: sha512-rdhoc7baSD7ZzcjavEpYN8gZJle1KhjEKj4SJeMgBpcnO4as7oXUVU4LtFpotzZdFlo57qaLrNzfvppSTsKvZQ==} + peerDependencies: + graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 + graphql-ws: ^5.5.5 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 + subscriptions-transport-ws: ^0.9.0 || ^0.11.0 + peerDependenciesMeta: + graphql-ws: + optional: true + react: + optional: true + react-dom: + optional: true + subscriptions-transport-ws: + optional: true + dependencies: + '@graphql-typed-document-node/core': 3.2.0(graphql@16.8.1) + '@wry/context': 0.7.4 + '@wry/equality': 0.5.7 + '@wry/trie': 0.4.3 + graphql: 16.8.1 + graphql-tag: 2.12.6(graphql@16.8.1) + hoist-non-react-statics: 3.3.2 + optimism: 0.16.2 + prop-types: 15.8.1 + response-iterator: 0.2.6 + symbol-observable: 4.0.0 + ts-invariant: 0.10.3 + tslib: 2.6.2 + zen-observable-ts: 1.2.5 + dev: false - /@babel/core@7.23.6: - resolution: {integrity: sha512-FxpRyGjrMJXh7X3wGLGhNDCRiwpWEF74sKjTLDJSG5Kyvow3QZaG0Adbqzi9ZrVjTWpsX+2cxWXD71NMg93kdw==} - engines: {node: '>=6.9.0'} + /@apollo/federation-internals@2.6.2(graphql@16.8.1): + resolution: {integrity: sha512-L5Ppl+FQ2+ETpJ8NCa7T8ifAjAX8K/4NW8N08d6TRUJu0M/8rvIL0CgX033Jno/+FVIFhNBbVN2kGoSKDl1YPQ==} + engines: {node: '>=14.15.0'} + peerDependencies: + graphql: ^16.5.0 dependencies: - '@ampproject/remapping': 2.2.1 - '@babel/code-frame': 7.23.5 - '@babel/generator': 7.23.6 - '@babel/helper-compilation-targets': 7.23.6 - '@babel/helper-module-transforms': 7.23.3(@babel/core@7.23.6) - '@babel/helpers': 7.23.6 - '@babel/parser': 7.23.6 - '@babel/template': 7.22.15 - '@babel/traverse': 7.23.6 - '@babel/types': 7.23.6 - convert-source-map: 2.0.0 - debug: 4.3.4 - gensync: 1.0.0-beta.2 - json5: 2.2.3 - semver: 6.3.1 + '@types/uuid': 9.0.7 + chalk: 4.1.2 + graphql: 16.8.1 + js-levenshtein: 1.1.6 + uuid: 9.0.1 + dev: false + + /@apollo/protobufjs@1.2.7: + resolution: {integrity: sha512-Lahx5zntHPZia35myYDBRuF58tlwPskwHc5CWBZC/4bMKB6siTBWwtMrkqXcsNwQiFSzSx5hKdRPUmemrEp3Gg==} + hasBin: true + requiresBuild: true + dependencies: + '@protobufjs/aspromise': 1.1.2 + '@protobufjs/base64': 1.1.2 + '@protobufjs/codegen': 2.0.4 + '@protobufjs/eventemitter': 1.1.0 + '@protobufjs/fetch': 1.1.0 + '@protobufjs/float': 1.0.2 + '@protobufjs/inquire': 1.1.0 + '@protobufjs/path': 1.1.2 + '@protobufjs/pool': 1.1.0 + '@protobufjs/utf8': 1.1.0 + '@types/long': 4.0.2 + long: 4.0.0 + dev: false + + /@apollo/server-gateway-interface@1.1.1(graphql@16.8.1): + resolution: {integrity: sha512-pGwCl/po6+rxRmDMFgozKQo2pbsSwE91TpsDBAOgf74CRDPXHHtM88wbwjab0wMMZh95QfR45GGyDIdhY24bkQ==} + peerDependencies: + graphql: 14.x || 15.x || 16.x + dependencies: + '@apollo/usage-reporting-protobuf': 4.1.1 + '@apollo/utils.fetcher': 2.0.1 + '@apollo/utils.keyvaluecache': 2.1.1 + '@apollo/utils.logger': 2.0.1 + graphql: 16.8.1 + dev: false + + /@apollo/server@4.9.5(graphql@16.8.1): + resolution: {integrity: sha512-eDBfArYbZaTm1AGa82M1aL7lOscVhnZsH85+OWmHMIR98qntzEjNpWpQPYDTru63Qxs4kHcY29NUx/kMGZfGEA==} + engines: {node: '>=14.16.0'} + peerDependencies: + graphql: ^16.6.0 + dependencies: + '@apollo/cache-control-types': 1.0.3(graphql@16.8.1) + '@apollo/server-gateway-interface': 1.1.1(graphql@16.8.1) + '@apollo/usage-reporting-protobuf': 4.1.1 + '@apollo/utils.createhash': 2.0.1 + '@apollo/utils.fetcher': 2.0.1 + '@apollo/utils.isnodelike': 2.0.1 + '@apollo/utils.keyvaluecache': 2.1.1 + '@apollo/utils.logger': 2.0.1 + '@apollo/utils.usagereporting': 2.1.0(graphql@16.8.1) + '@apollo/utils.withrequired': 2.0.1 + '@graphql-tools/schema': 9.0.19(graphql@16.8.1) + '@josephg/resolvable': 1.0.1 + '@types/express': 4.17.21 + '@types/express-serve-static-core': 4.17.41 + '@types/node-fetch': 2.6.10 + async-retry: 1.3.3 + body-parser: 1.20.2 + cors: 2.8.5 + express: 4.18.2 + graphql: 16.8.1 + loglevel: 1.8.1 + lru-cache: 7.18.3 + negotiator: 0.6.3 + node-abort-controller: 3.1.1 + node-fetch: 2.7.0 + uuid: 9.0.1 + whatwg-mimetype: 3.0.0 transitivePeerDependencies: + - encoding - supports-color - dev: true + dev: false - /@babel/eslint-parser@7.23.3(@babel/core@7.23.6)(eslint@8.56.0): - resolution: {integrity: sha512-9bTuNlyx7oSstodm1cR1bECj4fkiknsDa1YniISkJemMY3DGhJNYBECbe6QD/q54mp2J8VO66jW3/7uP//iFCw==} - engines: {node: ^10.13.0 || ^12.13.0 || >=14.0.0} + /@apollo/subgraph@2.6.2(graphql@16.8.1): + resolution: {integrity: sha512-TZXCq//fYv++gx8Tsngfa85fH6UGvpyz2xqGh9YIg2e3akeikRW77FRg0tfFXL9CwVmWLukav1hJGsipOklM1g==} + engines: {node: '>=14.15.0'} peerDependencies: - '@babel/core': ^7.11.0 - eslint: ^7.5.0 || ^8.0.0 + graphql: ^16.5.0 dependencies: - '@babel/core': 7.23.6 - '@nicolo-ribaudo/eslint-scope-5-internals': 5.1.1-v1 - eslint: 8.56.0 - eslint-visitor-keys: 2.1.0 - semver: 6.3.1 - dev: true + '@apollo/cache-control-types': 1.0.3(graphql@16.8.1) + '@apollo/federation-internals': 2.6.2(graphql@16.8.1) + graphql: 16.8.1 + dev: false - /@babel/generator@7.23.6: - resolution: {integrity: sha512-qrSfCYxYQB5owCmGLbl8XRpX1ytXlpueOb0N0UmQwA073KZxejgQTzAmJezxvpwQD9uGtK2shHdi55QT+MbjIw==} - engines: {node: '>=6.9.0'} + /@apollo/usage-reporting-protobuf@4.1.1: + resolution: {integrity: sha512-u40dIUePHaSKVshcedO7Wp+mPiZsaU6xjv9J+VyxpoU/zL6Jle+9zWeG98tr/+SZ0nZ4OXhrbb8SNr0rAPpIDA==} dependencies: - '@babel/types': 7.23.6 - '@jridgewell/gen-mapping': 0.3.3 - '@jridgewell/trace-mapping': 0.3.20 - jsesc: 2.5.2 - dev: true + '@apollo/protobufjs': 1.2.7 + dev: false - /@babel/helper-compilation-targets@7.23.6: - resolution: {integrity: sha512-9JB548GZoQVmzrFgp8o7KxdgkTGm6xs9DW0o/Pim72UDjzr5ObUQ6ZzYPqA+g9OTS2bBQoctLJrky0RDCAWRgQ==} - engines: {node: '>=6.9.0'} + /@apollo/utils.createhash@2.0.1: + resolution: {integrity: sha512-fQO4/ZOP8LcXWvMNhKiee+2KuKyqIcfHrICA+M4lj/h/Lh1H10ICcUtk6N/chnEo5HXu0yejg64wshdaiFitJg==} + engines: {node: '>=14'} dependencies: - '@babel/compat-data': 7.23.5 - '@babel/helper-validator-option': 7.23.5 - browserslist: 4.22.2 - lru-cache: 5.1.1 - semver: 6.3.1 - dev: true - - /@babel/helper-environment-visitor@7.22.20: - resolution: {integrity: sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==} - engines: {node: '>=6.9.0'} - dev: true + '@apollo/utils.isnodelike': 2.0.1 + sha.js: 2.4.11 + dev: false - /@babel/helper-function-name@7.23.0: - resolution: {integrity: sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==} - engines: {node: '>=6.9.0'} + /@apollo/utils.dropunuseddefinitions@2.0.1(graphql@16.8.1): + resolution: {integrity: sha512-EsPIBqsSt2BwDsv8Wu76LK5R1KtsVkNoO4b0M5aK0hx+dGg9xJXuqlr7Fo34Dl+y83jmzn+UvEW+t1/GP2melA==} + engines: {node: '>=14'} + peerDependencies: + graphql: 14.x || 15.x || 16.x dependencies: - '@babel/template': 7.22.15 - '@babel/types': 7.23.6 - dev: true + graphql: 16.8.1 + dev: false - /@babel/helper-hoist-variables@7.22.5: - resolution: {integrity: sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==} - engines: {node: '>=6.9.0'} + /@apollo/utils.fetcher@2.0.1: + resolution: {integrity: sha512-jvvon885hEyWXd4H6zpWeN3tl88QcWnHp5gWF5OPF34uhvoR+DFqcNxs9vrRaBBSY3qda3Qe0bdud7tz2zGx1A==} + engines: {node: '>=14'} + dev: false + + /@apollo/utils.isnodelike@2.0.1: + resolution: {integrity: sha512-w41XyepR+jBEuVpoRM715N2ZD0xMD413UiJx8w5xnAZD2ZkSJnMJBoIzauK83kJpSgNuR6ywbV29jG9NmxjK0Q==} + engines: {node: '>=14'} + dev: false + + /@apollo/utils.keyvaluecache@2.1.1: + resolution: {integrity: sha512-qVo5PvUUMD8oB9oYvq4ViCjYAMWnZ5zZwEjNF37L2m1u528x5mueMlU+Cr1UinupCgdB78g+egA1G98rbJ03Vw==} + engines: {node: '>=14'} dependencies: - '@babel/types': 7.23.6 - dev: true + '@apollo/utils.logger': 2.0.1 + lru-cache: 7.18.3 + dev: false - /@babel/helper-module-imports@7.22.15: - resolution: {integrity: sha512-0pYVBnDKZO2fnSPCrgM/6WMc7eS20Fbok+0r88fp+YtWVLZrp4CkafFGIp+W0VKw4a22sgebPT99y+FDNMdP4w==} - engines: {node: '>=6.9.0'} + /@apollo/utils.logger@2.0.1: + resolution: {integrity: sha512-YuplwLHaHf1oviidB7MxnCXAdHp3IqYV8n0momZ3JfLniae92eYqMIx+j5qJFX6WKJPs6q7bczmV4lXIsTu5Pg==} + engines: {node: '>=14'} + dev: false + + /@apollo/utils.printwithreducedwhitespace@2.0.1(graphql@16.8.1): + resolution: {integrity: sha512-9M4LUXV/fQBh8vZWlLvb/HyyhjJ77/I5ZKu+NBWV/BmYGyRmoEP9EVAy7LCVoY3t8BDcyCAGfxJaLFCSuQkPUg==} + engines: {node: '>=14'} + peerDependencies: + graphql: 14.x || 15.x || 16.x dependencies: - '@babel/types': 7.23.6 - dev: true + graphql: 16.8.1 + dev: false - /@babel/helper-module-transforms@7.23.3(@babel/core@7.23.6): - resolution: {integrity: sha512-7bBs4ED9OmswdfDzpz4MpWgSrV7FXlc3zIagvLFjS5H+Mk7Snr21vQ6QwrsoCGMfNC4e4LQPdoULEt4ykz0SRQ==} - engines: {node: '>=6.9.0'} + /@apollo/utils.removealiases@2.0.1(graphql@16.8.1): + resolution: {integrity: sha512-0joRc2HBO4u594Op1nev+mUF6yRnxoUH64xw8x3bX7n8QBDYdeYgY4tF0vJReTy+zdn2xv6fMsquATSgC722FA==} + engines: {node: '>=14'} peerDependencies: - '@babel/core': ^7.0.0 + graphql: 14.x || 15.x || 16.x dependencies: - '@babel/core': 7.23.6 - '@babel/helper-environment-visitor': 7.22.20 - '@babel/helper-module-imports': 7.22.15 - '@babel/helper-simple-access': 7.22.5 - '@babel/helper-split-export-declaration': 7.22.6 - '@babel/helper-validator-identifier': 7.22.20 - dev: true + graphql: 16.8.1 + dev: false - /@babel/helper-simple-access@7.22.5: - resolution: {integrity: sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w==} - engines: {node: '>=6.9.0'} + /@apollo/utils.sortast@2.0.1(graphql@16.8.1): + resolution: {integrity: sha512-eciIavsWpJ09za1pn37wpsCGrQNXUhM0TktnZmHwO+Zy9O4fu/WdB4+5BvVhFiZYOXvfjzJUcc+hsIV8RUOtMw==} + engines: {node: '>=14'} + peerDependencies: + graphql: 14.x || 15.x || 16.x dependencies: - '@babel/types': 7.23.6 - dev: true + graphql: 16.8.1 + lodash.sortby: 4.7.0 + dev: false - /@babel/helper-split-export-declaration@7.22.6: - resolution: {integrity: sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==} - engines: {node: '>=6.9.0'} + /@apollo/utils.stripsensitiveliterals@2.0.1(graphql@16.8.1): + resolution: {integrity: sha512-QJs7HtzXS/JIPMKWimFnUMK7VjkGQTzqD9bKD1h3iuPAqLsxd0mUNVbkYOPTsDhUKgcvUOfOqOJWYohAKMvcSA==} + engines: {node: '>=14'} + peerDependencies: + graphql: 14.x || 15.x || 16.x dependencies: - '@babel/types': 7.23.6 - dev: true + graphql: 16.8.1 + dev: false - /@babel/helper-string-parser@7.23.4: - resolution: {integrity: sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ==} - engines: {node: '>=6.9.0'} - dev: true + /@apollo/utils.usagereporting@2.1.0(graphql@16.8.1): + resolution: {integrity: sha512-LPSlBrn+S17oBy5eWkrRSGb98sWmnEzo3DPTZgp8IQc8sJe0prDgDuppGq4NeQlpoqEHz0hQeYHAOA0Z3aQsxQ==} + engines: {node: '>=14'} + peerDependencies: + graphql: 14.x || 15.x || 16.x + dependencies: + '@apollo/usage-reporting-protobuf': 4.1.1 + '@apollo/utils.dropunuseddefinitions': 2.0.1(graphql@16.8.1) + '@apollo/utils.printwithreducedwhitespace': 2.0.1(graphql@16.8.1) + '@apollo/utils.removealiases': 2.0.1(graphql@16.8.1) + '@apollo/utils.sortast': 2.0.1(graphql@16.8.1) + '@apollo/utils.stripsensitiveliterals': 2.0.1(graphql@16.8.1) + graphql: 16.8.1 + dev: false - /@babel/helper-validator-identifier@7.22.20: - resolution: {integrity: sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==} - engines: {node: '>=6.9.0'} - dev: true + /@apollo/utils.withrequired@2.0.1: + resolution: {integrity: sha512-YBDiuAX9i1lLc6GeTy1m7DGLFn/gMnvXqlalOIMjM7DeOgIacEjjfwPqb0M1CQ2v11HhR15d1NmxJoRCfrNqcA==} + engines: {node: '>=14'} + dev: false - /@babel/helper-validator-option@7.23.5: - resolution: {integrity: sha512-85ttAOMLsr53VgXkTbkx8oA6YTfT4q7/HzXSLEYmjcSTJPMPQtvq1BD79Byep5xMUYbGRzEpDsjUf3dyp54IKw==} - engines: {node: '>=6.9.0'} - dev: true + /@aws-crypto/crc32@3.0.0: + resolution: {integrity: sha512-IzSgsrxUcsrejQbPVilIKy16kAT52EwB6zSaI+M3xxIhKh5+aldEyvI+z6erM7TCLB2BJsFrtHjp6/4/sr+3dA==} + dependencies: + '@aws-crypto/util': 3.0.0 + '@aws-sdk/types': 3.391.0 + tslib: 1.14.1 + dev: false - /@babel/helpers@7.23.6: - resolution: {integrity: sha512-wCfsbN4nBidDRhpDhvcKlzHWCTlgJYUUdSJfzXb2NuBssDSIjc3xcb+znA7l+zYsFljAcGM0aFkN40cR3lXiGA==} - engines: {node: '>=6.9.0'} + /@aws-crypto/ie11-detection@3.0.0: + resolution: {integrity: sha512-341lBBkiY1DfDNKai/wXM3aujNBkXR7tq1URPQDL9wi3AUbI80NR74uF1TXHMm7po1AcnFk8iu2S2IeU/+/A+Q==} dependencies: - '@babel/template': 7.22.15 - '@babel/traverse': 7.23.6 - '@babel/types': 7.23.6 - transitivePeerDependencies: - - supports-color - dev: true + tslib: 1.14.1 + dev: false - /@babel/highlight@7.23.4: - resolution: {integrity: sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A==} - engines: {node: '>=6.9.0'} + /@aws-crypto/sha256-browser@3.0.0: + resolution: {integrity: sha512-8VLmW2B+gjFbU5uMeqtQM6Nj0/F1bro80xQXCW6CQBWgosFWXTx77aeOF5CAIAmbOK64SdMBJdNr6J41yP5mvQ==} dependencies: - '@babel/helper-validator-identifier': 7.22.20 - chalk: 2.4.2 - js-tokens: 4.0.0 - dev: true + '@aws-crypto/ie11-detection': 3.0.0 + '@aws-crypto/sha256-js': 3.0.0 + '@aws-crypto/supports-web-crypto': 3.0.0 + '@aws-crypto/util': 3.0.0 + '@aws-sdk/types': 3.391.0 + '@aws-sdk/util-locate-window': 3.465.0 + '@aws-sdk/util-utf8-browser': 3.259.0 + tslib: 1.14.1 + dev: false - /@babel/parser@7.23.6: - resolution: {integrity: sha512-Z2uID7YJ7oNvAI20O9X0bblw7Qqs8Q2hFy0R9tAfnfLkp5MW0UH9eUvnDSnFwKZ0AvgS1ucqR4KzvVHgnke1VQ==} - engines: {node: '>=6.0.0'} - hasBin: true + /@aws-crypto/sha256-js@3.0.0: + resolution: {integrity: sha512-PnNN7os0+yd1XvXAy23CFOmTbMaDxgxXtTKHybrJ39Y8kGzBATgBFibWJKH6BhytLI/Zyszs87xCOBNyBig6vQ==} dependencies: - '@babel/types': 7.23.6 - dev: true + '@aws-crypto/util': 3.0.0 + '@aws-sdk/types': 3.391.0 + tslib: 1.14.1 + dev: false - /@babel/runtime@7.23.6: - resolution: {integrity: sha512-zHd0eUrf5GZoOWVCXp6koAKQTfZV07eit6bGPmJgnZdnSAvvZee6zniW2XMF7Cmc4ISOOnPy3QaSiIJGJkVEDQ==} - engines: {node: '>=6.9.0'} + /@aws-crypto/supports-web-crypto@3.0.0: + resolution: {integrity: sha512-06hBdMwUAb2WFTuGG73LSC0wfPu93xWwo5vL2et9eymgmu3Id5vFAHBbajVWiGhPO37qcsdCap/FqXvJGJWPIg==} dependencies: - regenerator-runtime: 0.14.1 - dev: true + tslib: 1.14.1 + dev: false - /@babel/template@7.22.15: - resolution: {integrity: sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==} - engines: {node: '>=6.9.0'} + /@aws-crypto/util@3.0.0: + resolution: {integrity: sha512-2OJlpeJpCR48CC8r+uKVChzs9Iungj9wkZrl8Z041DWEWvyIHILYKCPNzJghKsivj+S3mLo6BVc7mBNzdxA46w==} dependencies: - '@babel/code-frame': 7.23.5 - '@babel/parser': 7.23.6 - '@babel/types': 7.23.6 - dev: true + '@aws-sdk/types': 3.391.0 + '@aws-sdk/util-utf8-browser': 3.259.0 + tslib: 1.14.1 + dev: false - /@babel/traverse@7.23.6: - resolution: {integrity: sha512-czastdK1e8YByZqezMPFiZ8ahwVMh/ESl9vPgvgdB9AmFMGP5jfpFax74AQgl5zj4XHzqeYAg2l8PuUeRS1MgQ==} - engines: {node: '>=6.9.0'} + /@aws-sdk/abort-controller@3.342.0: + resolution: {integrity: sha512-W1lAYldbzDjfn8vwnwNe+6qNWfSu1+JrdiVIRSwsiwKvF2ahjKuaLoc8rJM09C6ieNWRi5634urFgfwAJuv6vg==} + engines: {node: '>=14.0.0'} dependencies: - '@babel/code-frame': 7.23.5 - '@babel/generator': 7.23.6 - '@babel/helper-environment-visitor': 7.22.20 - '@babel/helper-function-name': 7.23.0 - '@babel/helper-hoist-variables': 7.22.5 - '@babel/helper-split-export-declaration': 7.22.6 - '@babel/parser': 7.23.6 - '@babel/types': 7.23.6 - debug: 4.3.4 - globals: 11.12.0 - transitivePeerDependencies: - - supports-color - dev: true + '@aws-sdk/types': 3.342.0 + tslib: 2.6.2 + dev: false - /@babel/types@7.23.6: - resolution: {integrity: sha512-+uarb83brBzPKN38NX1MkB6vb6+mwvR6amUulqAE7ccQw1pEl+bCia9TbdG1lsnFP7lZySvUn37CHyXQdfTwzg==} - engines: {node: '>=6.9.0'} + /@aws-sdk/client-dynamodb@3.391.0: + resolution: {integrity: sha512-3JDes5VeZ1jT8L+lq3zUQZUsWm+V8VTKyQzyJkCAOa69pguEo4qmPNVIbtpka83OYCBk3cFpZ1hjVnZAR164EQ==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-crypto/sha256-browser': 3.0.0 + '@aws-crypto/sha256-js': 3.0.0 + '@aws-sdk/client-sts': 3.391.0 + '@aws-sdk/credential-provider-node': 3.391.0 + '@aws-sdk/middleware-endpoint-discovery': 3.391.0 + '@aws-sdk/middleware-host-header': 3.391.0 + '@aws-sdk/middleware-logger': 3.391.0 + '@aws-sdk/middleware-recursion-detection': 3.391.0 + '@aws-sdk/middleware-signing': 3.391.0 + '@aws-sdk/middleware-user-agent': 3.391.0 + '@aws-sdk/types': 3.391.0 + '@aws-sdk/util-endpoints': 3.391.0 + '@aws-sdk/util-user-agent-browser': 3.391.0 + '@aws-sdk/util-user-agent-node': 3.391.0 + '@smithy/config-resolver': 2.0.21 + '@smithy/fetch-http-handler': 2.3.1 + '@smithy/hash-node': 2.0.17 + '@smithy/invalid-dependency': 2.0.15 + '@smithy/middleware-content-length': 2.0.17 + '@smithy/middleware-endpoint': 2.2.3 + '@smithy/middleware-retry': 2.0.25 + '@smithy/middleware-serde': 2.0.15 + '@smithy/middleware-stack': 2.0.9 + '@smithy/node-config-provider': 2.1.8 + '@smithy/node-http-handler': 2.2.1 + '@smithy/protocol-http': 2.0.5 + '@smithy/smithy-client': 2.2.0 + '@smithy/types': 2.7.0 + '@smithy/url-parser': 2.0.15 + '@smithy/util-base64': 2.0.1 + '@smithy/util-body-length-browser': 2.0.1 + '@smithy/util-body-length-node': 2.1.0 + '@smithy/util-defaults-mode-browser': 2.0.23 + '@smithy/util-defaults-mode-node': 2.0.30 + '@smithy/util-retry': 2.0.8 + '@smithy/util-utf8': 2.0.2 + '@smithy/util-waiter': 2.0.15 + tslib: 2.6.2 + uuid: 8.3.2 + transitivePeerDependencies: + - aws-crt + dev: false + + /@aws-sdk/client-eventbridge@3.342.0: + resolution: {integrity: sha512-Rhe+A+DgKfjhrjLdSaxE9RrsLbm6B5+UR++DODyc9OImjsWCwG4IzCZ5KOB6zO3m2+E+hUxskhtgiv0Rq8r2hg==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-crypto/sha256-browser': 3.0.0 + '@aws-crypto/sha256-js': 3.0.0 + '@aws-sdk/client-sts': 3.342.0 + '@aws-sdk/config-resolver': 3.342.0 + '@aws-sdk/credential-provider-node': 3.342.0 + '@aws-sdk/fetch-http-handler': 3.342.0 + '@aws-sdk/hash-node': 3.342.0 + '@aws-sdk/invalid-dependency': 3.342.0 + '@aws-sdk/middleware-content-length': 3.342.0 + '@aws-sdk/middleware-endpoint': 3.342.0 + '@aws-sdk/middleware-host-header': 3.342.0 + '@aws-sdk/middleware-logger': 3.342.0 + '@aws-sdk/middleware-recursion-detection': 3.342.0 + '@aws-sdk/middleware-retry': 3.342.0 + '@aws-sdk/middleware-serde': 3.342.0 + '@aws-sdk/middleware-signing': 3.342.0 + '@aws-sdk/middleware-stack': 3.342.0 + '@aws-sdk/middleware-user-agent': 3.342.0 + '@aws-sdk/node-config-provider': 3.342.0 + '@aws-sdk/node-http-handler': 3.342.0 + '@aws-sdk/signature-v4-multi-region': 3.342.0 + '@aws-sdk/smithy-client': 3.342.0 + '@aws-sdk/types': 3.342.0 + '@aws-sdk/url-parser': 3.342.0 + '@aws-sdk/util-base64': 3.310.0 + '@aws-sdk/util-body-length-browser': 3.310.0 + '@aws-sdk/util-body-length-node': 3.310.0 + '@aws-sdk/util-defaults-mode-browser': 3.342.0 + '@aws-sdk/util-defaults-mode-node': 3.342.0 + '@aws-sdk/util-endpoints': 3.342.0 + '@aws-sdk/util-retry': 3.342.0 + '@aws-sdk/util-user-agent-browser': 3.342.0 + '@aws-sdk/util-user-agent-node': 3.342.0 + '@aws-sdk/util-utf8': 3.310.0 + '@smithy/protocol-http': 1.2.0 + '@smithy/types': 1.2.0 + tslib: 2.6.2 + transitivePeerDependencies: + - '@aws-sdk/signature-v4-crt' + - aws-crt + dev: false + + /@aws-sdk/client-lambda@3.391.0: + resolution: {integrity: sha512-NulxmBFKIJ1l6GeKD+kYzQioi3HsIRKCWRzosNxtyhCwPRqFp9dzJ6S4024hEyeDN2q3OYknMz5oOQnQn2pvTg==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-crypto/sha256-browser': 3.0.0 + '@aws-crypto/sha256-js': 3.0.0 + '@aws-sdk/client-sts': 3.391.0 + '@aws-sdk/credential-provider-node': 3.391.0 + '@aws-sdk/middleware-host-header': 3.391.0 + '@aws-sdk/middleware-logger': 3.391.0 + '@aws-sdk/middleware-recursion-detection': 3.391.0 + '@aws-sdk/middleware-signing': 3.391.0 + '@aws-sdk/middleware-user-agent': 3.391.0 + '@aws-sdk/types': 3.391.0 + '@aws-sdk/util-endpoints': 3.391.0 + '@aws-sdk/util-user-agent-browser': 3.391.0 + '@aws-sdk/util-user-agent-node': 3.391.0 + '@smithy/config-resolver': 2.0.21 + '@smithy/eventstream-serde-browser': 2.0.15 + '@smithy/eventstream-serde-config-resolver': 2.0.15 + '@smithy/eventstream-serde-node': 2.0.15 + '@smithy/fetch-http-handler': 2.3.1 + '@smithy/hash-node': 2.0.17 + '@smithy/invalid-dependency': 2.0.15 + '@smithy/middleware-content-length': 2.0.17 + '@smithy/middleware-endpoint': 2.2.3 + '@smithy/middleware-retry': 2.0.25 + '@smithy/middleware-serde': 2.0.15 + '@smithy/middleware-stack': 2.0.9 + '@smithy/node-config-provider': 2.1.8 + '@smithy/node-http-handler': 2.2.1 + '@smithy/protocol-http': 2.0.5 + '@smithy/smithy-client': 2.2.0 + '@smithy/types': 2.7.0 + '@smithy/url-parser': 2.0.15 + '@smithy/util-base64': 2.0.1 + '@smithy/util-body-length-browser': 2.0.1 + '@smithy/util-body-length-node': 2.1.0 + '@smithy/util-defaults-mode-browser': 2.0.23 + '@smithy/util-defaults-mode-node': 2.0.30 + '@smithy/util-retry': 2.0.8 + '@smithy/util-stream': 2.0.23 + '@smithy/util-utf8': 2.0.2 + '@smithy/util-waiter': 2.0.15 + tslib: 2.6.2 + transitivePeerDependencies: + - aws-crt + dev: false + + /@aws-sdk/client-sso-oidc@3.342.0: + resolution: {integrity: sha512-C1jeKD39pWXlpGRxhWWBw2No1lyZnyIN72M2Qg3BWK6QlsSDtd9kdhpGS9rQU0i1F4w5x178a+qiGWHHMhCwLg==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-crypto/sha256-browser': 3.0.0 + '@aws-crypto/sha256-js': 3.0.0 + '@aws-sdk/config-resolver': 3.342.0 + '@aws-sdk/fetch-http-handler': 3.342.0 + '@aws-sdk/hash-node': 3.342.0 + '@aws-sdk/invalid-dependency': 3.342.0 + '@aws-sdk/middleware-content-length': 3.342.0 + '@aws-sdk/middleware-endpoint': 3.342.0 + '@aws-sdk/middleware-host-header': 3.342.0 + '@aws-sdk/middleware-logger': 3.342.0 + '@aws-sdk/middleware-recursion-detection': 3.342.0 + '@aws-sdk/middleware-retry': 3.342.0 + '@aws-sdk/middleware-serde': 3.342.0 + '@aws-sdk/middleware-stack': 3.342.0 + '@aws-sdk/middleware-user-agent': 3.342.0 + '@aws-sdk/node-config-provider': 3.342.0 + '@aws-sdk/node-http-handler': 3.342.0 + '@aws-sdk/smithy-client': 3.342.0 + '@aws-sdk/types': 3.342.0 + '@aws-sdk/url-parser': 3.342.0 + '@aws-sdk/util-base64': 3.310.0 + '@aws-sdk/util-body-length-browser': 3.310.0 + '@aws-sdk/util-body-length-node': 3.310.0 + '@aws-sdk/util-defaults-mode-browser': 3.342.0 + '@aws-sdk/util-defaults-mode-node': 3.342.0 + '@aws-sdk/util-endpoints': 3.342.0 + '@aws-sdk/util-retry': 3.342.0 + '@aws-sdk/util-user-agent-browser': 3.342.0 + '@aws-sdk/util-user-agent-node': 3.342.0 + '@aws-sdk/util-utf8': 3.310.0 + '@smithy/protocol-http': 1.2.0 + '@smithy/types': 1.2.0 + tslib: 2.6.2 + transitivePeerDependencies: + - aws-crt + dev: false + + /@aws-sdk/client-sso@3.342.0: + resolution: {integrity: sha512-DbEL+sWBua/04zTlJ6QmUsOpbeIlnPp8eYXQllCwsFzsIT04MjMI4hCZNia/weymwcq3vWTJOk2++SZf0sCGcw==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-crypto/sha256-browser': 3.0.0 + '@aws-crypto/sha256-js': 3.0.0 + '@aws-sdk/config-resolver': 3.342.0 + '@aws-sdk/fetch-http-handler': 3.342.0 + '@aws-sdk/hash-node': 3.342.0 + '@aws-sdk/invalid-dependency': 3.342.0 + '@aws-sdk/middleware-content-length': 3.342.0 + '@aws-sdk/middleware-endpoint': 3.342.0 + '@aws-sdk/middleware-host-header': 3.342.0 + '@aws-sdk/middleware-logger': 3.342.0 + '@aws-sdk/middleware-recursion-detection': 3.342.0 + '@aws-sdk/middleware-retry': 3.342.0 + '@aws-sdk/middleware-serde': 3.342.0 + '@aws-sdk/middleware-stack': 3.342.0 + '@aws-sdk/middleware-user-agent': 3.342.0 + '@aws-sdk/node-config-provider': 3.342.0 + '@aws-sdk/node-http-handler': 3.342.0 + '@aws-sdk/smithy-client': 3.342.0 + '@aws-sdk/types': 3.342.0 + '@aws-sdk/url-parser': 3.342.0 + '@aws-sdk/util-base64': 3.310.0 + '@aws-sdk/util-body-length-browser': 3.310.0 + '@aws-sdk/util-body-length-node': 3.310.0 + '@aws-sdk/util-defaults-mode-browser': 3.342.0 + '@aws-sdk/util-defaults-mode-node': 3.342.0 + '@aws-sdk/util-endpoints': 3.342.0 + '@aws-sdk/util-retry': 3.342.0 + '@aws-sdk/util-user-agent-browser': 3.342.0 + '@aws-sdk/util-user-agent-node': 3.342.0 + '@aws-sdk/util-utf8': 3.310.0 + '@smithy/protocol-http': 1.2.0 + '@smithy/types': 1.2.0 + tslib: 2.6.2 + transitivePeerDependencies: + - aws-crt + dev: false + + /@aws-sdk/client-sso@3.391.0: + resolution: {integrity: sha512-aT+O1CbWIWYlCtWK6g3ZaMvFNImOgFGurOEPscuedqzG5UQc1bRtRrGYShLyzcZgfXP+s0cKYJqgGeRNoWiwqA==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-crypto/sha256-browser': 3.0.0 + '@aws-crypto/sha256-js': 3.0.0 + '@aws-sdk/middleware-host-header': 3.391.0 + '@aws-sdk/middleware-logger': 3.391.0 + '@aws-sdk/middleware-recursion-detection': 3.391.0 + '@aws-sdk/middleware-user-agent': 3.391.0 + '@aws-sdk/types': 3.391.0 + '@aws-sdk/util-endpoints': 3.391.0 + '@aws-sdk/util-user-agent-browser': 3.391.0 + '@aws-sdk/util-user-agent-node': 3.391.0 + '@smithy/config-resolver': 2.0.21 + '@smithy/fetch-http-handler': 2.3.1 + '@smithy/hash-node': 2.0.17 + '@smithy/invalid-dependency': 2.0.15 + '@smithy/middleware-content-length': 2.0.17 + '@smithy/middleware-endpoint': 2.2.3 + '@smithy/middleware-retry': 2.0.25 + '@smithy/middleware-serde': 2.0.15 + '@smithy/middleware-stack': 2.0.9 + '@smithy/node-config-provider': 2.1.8 + '@smithy/node-http-handler': 2.2.1 + '@smithy/protocol-http': 2.0.5 + '@smithy/smithy-client': 2.2.0 + '@smithy/types': 2.7.0 + '@smithy/url-parser': 2.0.15 + '@smithy/util-base64': 2.0.1 + '@smithy/util-body-length-browser': 2.0.1 + '@smithy/util-body-length-node': 2.1.0 + '@smithy/util-defaults-mode-browser': 2.0.23 + '@smithy/util-defaults-mode-node': 2.0.30 + '@smithy/util-retry': 2.0.8 + '@smithy/util-utf8': 2.0.2 + tslib: 2.6.2 + transitivePeerDependencies: + - aws-crt + dev: false + + /@aws-sdk/client-sts@3.342.0: + resolution: {integrity: sha512-MUgYm/2ra1Pwoqw9ng75rVsvTLQvLHZLsTjJuKJ4hnHx1GdmQt4/ZlG1q/J2ZK2o6RZXqgavscz/nyrZH0QumA==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-crypto/sha256-browser': 3.0.0 + '@aws-crypto/sha256-js': 3.0.0 + '@aws-sdk/config-resolver': 3.342.0 + '@aws-sdk/credential-provider-node': 3.342.0 + '@aws-sdk/fetch-http-handler': 3.342.0 + '@aws-sdk/hash-node': 3.342.0 + '@aws-sdk/invalid-dependency': 3.342.0 + '@aws-sdk/middleware-content-length': 3.342.0 + '@aws-sdk/middleware-endpoint': 3.342.0 + '@aws-sdk/middleware-host-header': 3.342.0 + '@aws-sdk/middleware-logger': 3.342.0 + '@aws-sdk/middleware-recursion-detection': 3.342.0 + '@aws-sdk/middleware-retry': 3.342.0 + '@aws-sdk/middleware-sdk-sts': 3.342.0 + '@aws-sdk/middleware-serde': 3.342.0 + '@aws-sdk/middleware-signing': 3.342.0 + '@aws-sdk/middleware-stack': 3.342.0 + '@aws-sdk/middleware-user-agent': 3.342.0 + '@aws-sdk/node-config-provider': 3.342.0 + '@aws-sdk/node-http-handler': 3.342.0 + '@aws-sdk/smithy-client': 3.342.0 + '@aws-sdk/types': 3.342.0 + '@aws-sdk/url-parser': 3.342.0 + '@aws-sdk/util-base64': 3.310.0 + '@aws-sdk/util-body-length-browser': 3.310.0 + '@aws-sdk/util-body-length-node': 3.310.0 + '@aws-sdk/util-defaults-mode-browser': 3.342.0 + '@aws-sdk/util-defaults-mode-node': 3.342.0 + '@aws-sdk/util-endpoints': 3.342.0 + '@aws-sdk/util-retry': 3.342.0 + '@aws-sdk/util-user-agent-browser': 3.342.0 + '@aws-sdk/util-user-agent-node': 3.342.0 + '@aws-sdk/util-utf8': 3.310.0 + '@smithy/protocol-http': 1.2.0 + '@smithy/types': 1.2.0 + fast-xml-parser: 4.1.2 + tslib: 2.6.2 + transitivePeerDependencies: + - aws-crt + dev: false + + /@aws-sdk/client-sts@3.391.0: + resolution: {integrity: sha512-y+KmorcUx9o5O99sXVPbhGUpsLpfhzYRaYCqxArLsyzZTCO6XDXMi8vg/xtS+b703j9lWEl5GxAv2oBaEwEnhQ==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-crypto/sha256-browser': 3.0.0 + '@aws-crypto/sha256-js': 3.0.0 + '@aws-sdk/credential-provider-node': 3.391.0 + '@aws-sdk/middleware-host-header': 3.391.0 + '@aws-sdk/middleware-logger': 3.391.0 + '@aws-sdk/middleware-recursion-detection': 3.391.0 + '@aws-sdk/middleware-sdk-sts': 3.391.0 + '@aws-sdk/middleware-signing': 3.391.0 + '@aws-sdk/middleware-user-agent': 3.391.0 + '@aws-sdk/types': 3.391.0 + '@aws-sdk/util-endpoints': 3.391.0 + '@aws-sdk/util-user-agent-browser': 3.391.0 + '@aws-sdk/util-user-agent-node': 3.391.0 + '@smithy/config-resolver': 2.0.21 + '@smithy/fetch-http-handler': 2.3.1 + '@smithy/hash-node': 2.0.17 + '@smithy/invalid-dependency': 2.0.15 + '@smithy/middleware-content-length': 2.0.17 + '@smithy/middleware-endpoint': 2.2.3 + '@smithy/middleware-retry': 2.0.25 + '@smithy/middleware-serde': 2.0.15 + '@smithy/middleware-stack': 2.0.9 + '@smithy/node-config-provider': 2.1.8 + '@smithy/node-http-handler': 2.2.1 + '@smithy/protocol-http': 2.0.5 + '@smithy/smithy-client': 2.2.0 + '@smithy/types': 2.7.0 + '@smithy/url-parser': 2.0.15 + '@smithy/util-base64': 2.0.1 + '@smithy/util-body-length-browser': 2.0.1 + '@smithy/util-body-length-node': 2.1.0 + '@smithy/util-defaults-mode-browser': 2.0.23 + '@smithy/util-defaults-mode-node': 2.0.30 + '@smithy/util-retry': 2.0.8 + '@smithy/util-utf8': 2.0.2 + fast-xml-parser: 4.2.5 + tslib: 2.6.2 + transitivePeerDependencies: + - aws-crt + dev: false + + /@aws-sdk/config-resolver@3.342.0: + resolution: {integrity: sha512-jUg6DTTrCvG8AOPv5NRJ6PSQSC5fEI2gVv4luzvrGkRJULYbIqpdfUYdW7jB3rWAWC79pQQr5lSqC5DWH91stw==} + engines: {node: '>=14.0.0'} dependencies: - '@babel/helper-string-parser': 7.23.4 - '@babel/helper-validator-identifier': 7.22.20 - to-fast-properties: 2.0.0 - dev: true + '@aws-sdk/types': 3.342.0 + '@aws-sdk/util-config-provider': 3.310.0 + '@aws-sdk/util-middleware': 3.342.0 + tslib: 2.6.2 + dev: false - /@eslint-community/eslint-utils@4.4.0(eslint@8.56.0): - resolution: {integrity: sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - peerDependencies: - eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + /@aws-sdk/credential-provider-env@3.342.0: + resolution: {integrity: sha512-mufOcoqdXZXkvA7u6hUcJz6wKpVaho8SRWCvJrGO4YkyudUAoI9KSP5R4U+gtneDJ2Y/IEKPuw8ugNfANa1J+A==} + engines: {node: '>=14.0.0'} dependencies: - eslint: 8.56.0 - eslint-visitor-keys: 3.4.3 - dev: true + '@aws-sdk/property-provider': 3.342.0 + '@aws-sdk/types': 3.342.0 + tslib: 2.6.2 + dev: false - /@eslint-community/regexpp@4.10.0: - resolution: {integrity: sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA==} - engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} - dev: true + /@aws-sdk/credential-provider-env@3.391.0: + resolution: {integrity: sha512-mAzICedcg4bfL0mM5O6QTd9mQ331NLse1DMr6XL21ZZiLB48ej19L7AGV2xq5QwVbqKU3IVv1myRyhvpDM9jMg==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/types': 3.391.0 + '@smithy/property-provider': 2.0.16 + '@smithy/types': 2.7.0 + tslib: 2.6.2 + dev: false - /@eslint/eslintrc@2.1.4: - resolution: {integrity: sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + /@aws-sdk/credential-provider-imds@3.342.0: + resolution: {integrity: sha512-ReaHwFLfcsEYjDFvi95OFd+IU8frPwuAygwL56aiMT7Voc0oy3EqB3MFs3gzFxdLsJ0vw9TZMRbaouepAEVCkA==} + engines: {node: '>=14.0.0'} dependencies: - ajv: 6.12.6 - debug: 4.3.4 - espree: 9.6.1 - globals: 13.24.0 - ignore: 5.3.0 - import-fresh: 3.3.0 - js-yaml: 4.1.0 - minimatch: 3.1.2 - strip-json-comments: 3.1.1 + '@aws-sdk/node-config-provider': 3.342.0 + '@aws-sdk/property-provider': 3.342.0 + '@aws-sdk/types': 3.342.0 + '@aws-sdk/url-parser': 3.342.0 + tslib: 2.6.2 + dev: false + + /@aws-sdk/credential-provider-ini@3.342.0: + resolution: {integrity: sha512-VJ7+IlI3rx5XfO8AarbKeqNVwfExsWW0S6fqBXIim0s10FJAy7R+wxYyhZhawfRm0ydCggT+Ji6dftS+WXF8fg==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/credential-provider-env': 3.342.0 + '@aws-sdk/credential-provider-imds': 3.342.0 + '@aws-sdk/credential-provider-process': 3.342.0 + '@aws-sdk/credential-provider-sso': 3.342.0 + '@aws-sdk/credential-provider-web-identity': 3.342.0 + '@aws-sdk/property-provider': 3.342.0 + '@aws-sdk/shared-ini-file-loader': 3.342.0 + '@aws-sdk/types': 3.342.0 + tslib: 2.6.2 transitivePeerDependencies: - - supports-color - dev: true + - aws-crt + dev: false - /@eslint/js@8.56.0: - resolution: {integrity: sha512-gMsVel9D7f2HLkBma9VbtzZRehRogVRfbr++f06nL2vnCGCNlzOD+/MUov/F4p8myyAHspEhVobgjpX64q5m6A==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - dev: true + /@aws-sdk/credential-provider-ini@3.391.0: + resolution: {integrity: sha512-DJZmbmRMqNSfSV7UF8eBVhADz16KAMCTxnFuvgioHHfYUTZQEhCxRHI8jJqYWxhLTriS7AuTBIWr+1AIbwsCTA==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/credential-provider-env': 3.391.0 + '@aws-sdk/credential-provider-process': 3.391.0 + '@aws-sdk/credential-provider-sso': 3.391.0 + '@aws-sdk/credential-provider-web-identity': 3.391.0 + '@aws-sdk/types': 3.391.0 + '@smithy/credential-provider-imds': 2.1.4 + '@smithy/property-provider': 2.0.16 + '@smithy/shared-ini-file-loader': 2.2.7 + '@smithy/types': 2.7.0 + tslib: 2.6.2 + transitivePeerDependencies: + - aws-crt + dev: false - /@humanwhocodes/config-array@0.11.13: - resolution: {integrity: sha512-JSBDMiDKSzQVngfRjOdFXgFfklaXI4K9nLF49Auh21lmBWRLIK3+xTErTWD4KU54pb6coM6ESE7Awz/FNU3zgQ==} - engines: {node: '>=10.10.0'} - dependencies: - '@humanwhocodes/object-schema': 2.0.1 - debug: 4.3.4 - minimatch: 3.1.2 + /@aws-sdk/credential-provider-node@3.342.0: + resolution: {integrity: sha512-u3oUo0UxGEaHLtIx7a38aFLgcTe1OevCNe5exL3ugf5C4ifvUjM8rLWySQ9zrKRgPT2yDRYG/oq4ezjoR9fhHg==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/credential-provider-env': 3.342.0 + '@aws-sdk/credential-provider-imds': 3.342.0 + '@aws-sdk/credential-provider-ini': 3.342.0 + '@aws-sdk/credential-provider-process': 3.342.0 + '@aws-sdk/credential-provider-sso': 3.342.0 + '@aws-sdk/credential-provider-web-identity': 3.342.0 + '@aws-sdk/property-provider': 3.342.0 + '@aws-sdk/shared-ini-file-loader': 3.342.0 + '@aws-sdk/types': 3.342.0 + tslib: 2.6.2 transitivePeerDependencies: - - supports-color - dev: true + - aws-crt + dev: false - /@humanwhocodes/module-importer@1.0.1: - resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} - engines: {node: '>=12.22'} - dev: true + /@aws-sdk/credential-provider-node@3.391.0: + resolution: {integrity: sha512-LXHQwsTw4WBwRzD9swu8254Hao5MoIaGXIzbhX4EQ84dtOkKYbwiY4pDpLfcHcw3B1lFKkVclMze8WAs4EdEww==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/credential-provider-env': 3.391.0 + '@aws-sdk/credential-provider-ini': 3.391.0 + '@aws-sdk/credential-provider-process': 3.391.0 + '@aws-sdk/credential-provider-sso': 3.391.0 + '@aws-sdk/credential-provider-web-identity': 3.391.0 + '@aws-sdk/types': 3.391.0 + '@smithy/credential-provider-imds': 2.1.4 + '@smithy/property-provider': 2.0.16 + '@smithy/shared-ini-file-loader': 2.2.7 + '@smithy/types': 2.7.0 + tslib: 2.6.2 + transitivePeerDependencies: + - aws-crt + dev: false - /@humanwhocodes/object-schema@2.0.1: - resolution: {integrity: sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK+1GWi/yTvvLDHpoxLr0xxxeslWw==} - dev: true + /@aws-sdk/credential-provider-process@3.342.0: + resolution: {integrity: sha512-q03yJQPa4jnZtwKFW3yEYNMcpYH7wQzbEOEXjnXG4v8935oOttZjXBvRK7ax+f0D1ZHZFeFSashjw0A/bi1efQ==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/property-provider': 3.342.0 + '@aws-sdk/shared-ini-file-loader': 3.342.0 + '@aws-sdk/types': 3.342.0 + tslib: 2.6.2 + dev: false - /@jridgewell/gen-mapping@0.3.3: - resolution: {integrity: sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==} - engines: {node: '>=6.0.0'} + /@aws-sdk/credential-provider-process@3.391.0: + resolution: {integrity: sha512-KMlzPlBI+hBmXDo+EoFZdLgCVRkRa9B9iEE6x0+hQQ6g9bW6HI7cDRVdceR1ZoPasSaNAZ9QOXMTIBxTpn0sPQ==} + engines: {node: '>=14.0.0'} dependencies: - '@jridgewell/set-array': 1.1.2 - '@jridgewell/sourcemap-codec': 1.4.15 - '@jridgewell/trace-mapping': 0.3.20 - dev: true + '@aws-sdk/types': 3.391.0 + '@smithy/property-provider': 2.0.16 + '@smithy/shared-ini-file-loader': 2.2.7 + '@smithy/types': 2.7.0 + tslib: 2.6.2 + dev: false - /@jridgewell/resolve-uri@3.1.1: - resolution: {integrity: sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==} - engines: {node: '>=6.0.0'} - dev: true + /@aws-sdk/credential-provider-sso@3.342.0: + resolution: {integrity: sha512-ank2703Riz5gwTxC11FDnZtMcq1Z1JjN3Nd53ahyZ+KOJPgWXEw+uolEuzMl4oAovmbTJ6WANo2qMVmLzZEaQg==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/client-sso': 3.342.0 + '@aws-sdk/property-provider': 3.342.0 + '@aws-sdk/shared-ini-file-loader': 3.342.0 + '@aws-sdk/token-providers': 3.342.0 + '@aws-sdk/types': 3.342.0 + tslib: 2.6.2 + transitivePeerDependencies: + - aws-crt + dev: false - /@jridgewell/set-array@1.1.2: - resolution: {integrity: sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==} - engines: {node: '>=6.0.0'} - dev: true + /@aws-sdk/credential-provider-sso@3.391.0: + resolution: {integrity: sha512-FT/WoiRHiKys+FcRwvjui0yKuzNtJdn2uGuI1hYE0gpW1wVmW02ouufLckJTmcw09THUZ4w53OoCVU5OY00p8A==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/client-sso': 3.391.0 + '@aws-sdk/token-providers': 3.391.0 + '@aws-sdk/types': 3.391.0 + '@smithy/property-provider': 2.0.16 + '@smithy/shared-ini-file-loader': 2.2.7 + '@smithy/types': 2.7.0 + tslib: 2.6.2 + transitivePeerDependencies: + - aws-crt + dev: false - /@jridgewell/sourcemap-codec@1.4.15: - resolution: {integrity: sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==} - dev: true + /@aws-sdk/credential-provider-web-identity@3.342.0: + resolution: {integrity: sha512-+an5oGnzoXMmGJql0Qs9MtyQTmz5GFqrWleQ0k9UVhN3uIfCS9AITS7vb+q1+G7A7YXy9+KshgBhcHco0G/JWQ==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/property-provider': 3.342.0 + '@aws-sdk/types': 3.342.0 + tslib: 2.6.2 + dev: false - /@jridgewell/trace-mapping@0.3.20: - resolution: {integrity: sha512-R8LcPeWZol2zR8mmH3JeKQ6QRCFb7XgUhV9ZlGhHLGyg4wpPiPZNQOOWhFZhxKw8u//yTbNGI42Bx/3paXEQ+Q==} + /@aws-sdk/credential-provider-web-identity@3.391.0: + resolution: {integrity: sha512-n0vYg82B8bc4rxKltVbVqclev7hx+elyS9pEnZs3YbnbWJq0qqsznXmDfLqd1TcWpa09PGXcah0nsRDolVThsA==} + engines: {node: '>=14.0.0'} dependencies: - '@jridgewell/resolve-uri': 3.1.1 - '@jridgewell/sourcemap-codec': 1.4.15 - dev: true + '@aws-sdk/types': 3.391.0 + '@smithy/property-provider': 2.0.16 + '@smithy/types': 2.7.0 + tslib: 2.6.2 + dev: false - /@microsoft/tsdoc-config@0.16.2: - resolution: {integrity: sha512-OGiIzzoBLgWWR0UdRJX98oYO+XKGf7tiK4Zk6tQ/E4IJqGCe7dvkTvgDZV5cFJUzLGDOjeAXrnZoA6QkVySuxw==} + /@aws-sdk/endpoint-cache@3.310.0: + resolution: {integrity: sha512-y3wipforet41EDTI0vnzxILqwAGll1KfI5qcdX9pXF/WF1f+3frcOtPiWtQEZQpy4czRogKm3BHo70QBYAZxlQ==} + engines: {node: '>=14.0.0'} dependencies: - '@microsoft/tsdoc': 0.14.2 - ajv: 6.12.6 - jju: 1.4.0 - resolve: 1.19.0 - dev: true + mnemonist: 0.38.3 + tslib: 2.6.2 + dev: false - /@microsoft/tsdoc@0.14.2: - resolution: {integrity: sha512-9b8mPpKrfeGRuhFH5iO1iwCLeIIsV6+H1sRfxbkoGXIyQE2BTsPd9zqSqQJ+pv5sJ/hT5M1zvOFL02MnEezFug==} - dev: true + /@aws-sdk/eventstream-codec@3.342.0: + resolution: {integrity: sha512-IwtvSuplioMyiu/pQgpazKkGWDM5M5BOx85zmsB0uNxt6rmje8+WqPmGmuPdmJv4bLC5dJPLovcCp/fuH8XWhA==} + dependencies: + '@aws-crypto/crc32': 3.0.0 + '@aws-sdk/types': 3.342.0 + '@aws-sdk/util-hex-encoding': 3.310.0 + tslib: 2.6.2 + dev: false - /@nicolo-ribaudo/eslint-scope-5-internals@5.1.1-v1: - resolution: {integrity: sha512-54/JRvkLIzzDWshCWfuhadfrfZVPiElY8Fcgmg1HroEly/EDSszzhBAsarCux+D/kOslTRquNzuyGSmUSTTHGg==} + /@aws-sdk/fetch-http-handler@3.342.0: + resolution: {integrity: sha512-zsC23VUQMHEu4OKloLCVyWLG0ns6n+HKZ9euGLnNO3l0VSRne9qj/94yR+4jr/h04M7MhGf9mlczGfnZUFxs5w==} dependencies: - eslint-scope: 5.1.1 - dev: true + '@aws-sdk/protocol-http': 3.342.0 + '@aws-sdk/querystring-builder': 3.342.0 + '@aws-sdk/types': 3.342.0 + '@aws-sdk/util-base64': 3.310.0 + tslib: 2.6.2 + dev: false - /@nodelib/fs.scandir@2.1.5: - resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} - engines: {node: '>= 8'} + /@aws-sdk/hash-node@3.342.0: + resolution: {integrity: sha512-cFgXy9CDNQdYCdJBsG91FF0P0tNkCfi7+vTy7fzAEchxLxhcfLtC0cS6+gv2e3Dy8mv+uqp45Tu24+8Trx9hJQ==} + engines: {node: '>=14.0.0'} dependencies: - '@nodelib/fs.stat': 2.0.5 - run-parallel: 1.2.0 - dev: true + '@aws-sdk/types': 3.342.0 + '@aws-sdk/util-buffer-from': 3.310.0 + '@aws-sdk/util-utf8': 3.310.0 + tslib: 2.6.2 + dev: false - /@nodelib/fs.stat@2.0.5: - resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} - engines: {node: '>= 8'} - dev: true + /@aws-sdk/invalid-dependency@3.342.0: + resolution: {integrity: sha512-3qza2Br1jGKJi8toPYG9u5aGJ3sbGmJLgKDvlga7q3F8JaeB92He6muRJ07eyDvxZ9jiKhLZ2mtYoVcEjI7Mgw==} + dependencies: + '@aws-sdk/types': 3.342.0 + tslib: 2.6.2 + dev: false - /@nodelib/fs.walk@1.2.8: - resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} - engines: {node: '>= 8'} + /@aws-sdk/is-array-buffer@3.310.0: + resolution: {integrity: sha512-urnbcCR+h9NWUnmOtet/s4ghvzsidFmspfhYaHAmSRdy9yDjdjBJMFjjsn85A1ODUktztm+cVncXjQ38WCMjMQ==} + engines: {node: '>=14.0.0'} dependencies: - '@nodelib/fs.scandir': 2.1.5 - fastq: 1.16.0 - dev: true + tslib: 2.6.2 + dev: false - /@pkgr/core@0.1.0: - resolution: {integrity: sha512-Zwq5OCzuwJC2jwqmpEQt7Ds1DTi6BWSwoGkbb1n9pO3hzb35BoJELx7c0T23iDkBGkh2e7tvOtjF3tr3OaQHDQ==} - engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} - dev: true + /@aws-sdk/lib-dynamodb@3.391.0(@aws-sdk/client-dynamodb@3.391.0): + resolution: {integrity: sha512-PVhjLHjmiFqBIP4FK6gtpIQGdmZJYHinewJivPooSQwZM6Elzn4T1Vr0/rFmWKQUicru6bdMALFefGg6GlzF9w==} + engines: {node: '>=14.0.0'} + peerDependencies: + '@aws-sdk/client-dynamodb': ^3.0.0 + dependencies: + '@aws-sdk/client-dynamodb': 3.391.0 + '@aws-sdk/util-dynamodb': 3.391.0 + tslib: 2.6.2 + dev: false - /@rushstack/eslint-patch@1.6.1: - resolution: {integrity: sha512-UY+FGM/2jjMkzQLn8pxcHGMaVLh9aEitG3zY2CiY7XHdLiz3bZOwa6oDxNqEMv7zZkV+cj5DOdz0cQ1BP5Hjgw==} - dev: true + /@aws-sdk/middleware-content-length@3.342.0: + resolution: {integrity: sha512-7LUMZqhihSAptGRFFQvuwt9nCLNzNPkGd1oU1RpVXw6YPQfKP9Ec5tgg4oUlv1t58IYQvdVj5ITKp4X2aUJVPg==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/protocol-http': 3.342.0 + '@aws-sdk/types': 3.342.0 + tslib: 2.6.2 + dev: false - /@types/json-schema@7.0.15: - resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} - dev: true + /@aws-sdk/middleware-endpoint-discovery@3.391.0: + resolution: {integrity: sha512-HoqCuhRyBqDikVBuXwYRRxuTrMpYpg6LBGR3F9Zhdu+18AUui9eUIEd2VazHSq5uojeVhPrdlbB8sxT5TJWNlQ==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/endpoint-cache': 3.310.0 + '@aws-sdk/types': 3.391.0 + '@smithy/protocol-http': 2.0.5 + '@smithy/types': 2.7.0 + tslib: 2.6.2 + dev: false - /@types/json5@0.0.29: - resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} - dev: true + /@aws-sdk/middleware-endpoint@3.342.0: + resolution: {integrity: sha512-/rE+3a2EbNQoylc7vyN+O6GFfcLitboZ8f/Kdkld3Ijcp9whPHdfjiqujlwyiUTgBVP3BqgyB3r7AZDloc7B0g==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/middleware-serde': 3.342.0 + '@aws-sdk/types': 3.342.0 + '@aws-sdk/url-parser': 3.342.0 + '@aws-sdk/util-middleware': 3.342.0 + tslib: 2.6.2 + dev: false - /@types/normalize-package-data@2.4.4: - resolution: {integrity: sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==} - dev: true + /@aws-sdk/middleware-host-header@3.342.0: + resolution: {integrity: sha512-EOoix2D2Mk3NQtv7UVhJttfttGYechQxKuGvCI8+8iEKxqlyXaKqAkLR07BQb6epMYeKP4z1PfJm203Sf0WPUQ==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/protocol-http': 3.342.0 + '@aws-sdk/types': 3.342.0 + tslib: 2.6.2 + dev: false - /@types/semver@7.5.6: - resolution: {integrity: sha512-dn1l8LaMea/IjDoHNd9J52uBbInB796CDffS6VdIxvqYCPSG0V0DzHp76GpaWnlhg88uYyPbXCDIowa86ybd5A==} - dev: true + /@aws-sdk/middleware-host-header@3.391.0: + resolution: {integrity: sha512-+nyNr0rb2ixY7mU48nibr7L7gsw37y4oELhqgnNKhcjZDJ34imBwKIMFa64n21FdftmhcjR8IdSpzXE9xrkJ8g==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/types': 3.391.0 + '@smithy/protocol-http': 2.0.5 + '@smithy/types': 2.7.0 + tslib: 2.6.2 + dev: false - /@typescript-eslint/eslint-plugin@5.62.0(@typescript-eslint/parser@5.62.0)(eslint@8.56.0)(typescript@5.3.3): - resolution: {integrity: sha512-TiZzBSJja/LbhNPvk6yc0JrX9XqhQ0hdh6M2svYfsHGejaKFIAGd9MQ+ERIMzLGlN/kZoYIgdxFV0PuljTKXag==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - peerDependencies: - '@typescript-eslint/parser': ^5.0.0 - eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true + /@aws-sdk/middleware-logger@3.342.0: + resolution: {integrity: sha512-wbkp85T7p9sHLNPMY6HAXHvLOp+vOubFT/XLIGtgRhYu5aRJSlVo9qlwtdZjyhEgIRQ6H/QUnqAN7Zgk5bCLSw==} + engines: {node: '>=14.0.0'} dependencies: - '@eslint-community/regexpp': 4.10.0 - '@typescript-eslint/parser': 5.62.0(eslint@8.56.0)(typescript@5.3.3) - '@typescript-eslint/scope-manager': 5.62.0 - '@typescript-eslint/type-utils': 5.62.0(eslint@8.56.0)(typescript@5.3.3) - '@typescript-eslint/utils': 5.62.0(eslint@8.56.0)(typescript@5.3.3) - debug: 4.3.4 - eslint: 8.56.0 - graphemer: 1.4.0 - ignore: 5.3.0 - natural-compare-lite: 1.4.0 - semver: 7.5.4 - tsutils: 3.21.0(typescript@5.3.3) - typescript: 5.3.3 - transitivePeerDependencies: - - supports-color - dev: true + '@aws-sdk/types': 3.342.0 + tslib: 2.6.2 + dev: false + + /@aws-sdk/middleware-logger@3.391.0: + resolution: {integrity: sha512-KOwl5zo16b17JDhqILHBStccBQ2w35em7+/6vdkJdUII6OU8aVIFTlIQT9wOUvd4do6biIRBMZG3IK0Rg7mRDQ==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/types': 3.391.0 + '@smithy/types': 2.7.0 + tslib: 2.6.2 + dev: false + + /@aws-sdk/middleware-recursion-detection@3.342.0: + resolution: {integrity: sha512-KUDseSAz95kXCqnXEQxNObpviZ6F7eJ5lEgpi+ZehlzGDk/GyOVgjVuAyI7nNxWI5v0ZJ5nIDy+BH273dWbnmQ==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/protocol-http': 3.342.0 + '@aws-sdk/types': 3.342.0 + tslib: 2.6.2 + dev: false + + /@aws-sdk/middleware-recursion-detection@3.391.0: + resolution: {integrity: sha512-hVR3z59G7pX4pjDQs9Ag1tMgbLeGXOzeAAaNP9fEtHSd3KBMAGQgN3K3b9WPjzE2W0EoloHRJMK4qxZErdde2g==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/types': 3.391.0 + '@smithy/protocol-http': 2.0.5 + '@smithy/types': 2.7.0 + tslib: 2.6.2 + dev: false + + /@aws-sdk/middleware-retry@3.342.0: + resolution: {integrity: sha512-Bfllrjqs0bXNG7A3ydLjTAE5zPEdigG+/lDuEsCfB35gywZnnxqi6BjTeQ9Ss6gbEWX+WyXP7/oVdNaUDQUr9Q==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/protocol-http': 3.342.0 + '@aws-sdk/service-error-classification': 3.342.0 + '@aws-sdk/types': 3.342.0 + '@aws-sdk/util-middleware': 3.342.0 + '@aws-sdk/util-retry': 3.342.0 + tslib: 2.6.2 + uuid: 8.3.2 + dev: false + + /@aws-sdk/middleware-sdk-sts@3.342.0: + resolution: {integrity: sha512-eGcGDC+6UWKC87mex3voBVRcZN3hzFN6GVzWkTS574hDqp/uJG3yPk3Dltw0qf8skikTGi3/ZE+yAxerq/f5rg==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/middleware-signing': 3.342.0 + '@aws-sdk/types': 3.342.0 + tslib: 2.6.2 + dev: false + + /@aws-sdk/middleware-sdk-sts@3.391.0: + resolution: {integrity: sha512-6ZXI3Z4QU+TnT5PwKWloGmRHG81tWeI18/zxf9wWzrO2NhYFvITzEJH0vWLLiXdWtn/BYfLULXtDvkTaepbI5A==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/middleware-signing': 3.391.0 + '@aws-sdk/types': 3.391.0 + '@smithy/types': 2.7.0 + tslib: 2.6.2 + dev: false + + /@aws-sdk/middleware-serde@3.342.0: + resolution: {integrity: sha512-WRD+Cyu6+h1ymfPnAw4fI2q3zXjihJ55HFe1uRF8VPN4uBbJNfN3IqL38y/SMEdZ0gH9zNlRNxZLhR0q6SNZEQ==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/types': 3.342.0 + tslib: 2.6.2 + dev: false + + /@aws-sdk/middleware-signing@3.342.0: + resolution: {integrity: sha512-CFRQyPv4OjRGmFoB3OfKcQ0aHgS9VWC0YwoHnSWIcLt3Xltorug/Amk0obr/MFoIrktdlVtmvLEJ4Z+8cdsz8g==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/property-provider': 3.342.0 + '@aws-sdk/protocol-http': 3.342.0 + '@aws-sdk/signature-v4': 3.342.0 + '@aws-sdk/types': 3.342.0 + '@aws-sdk/util-middleware': 3.342.0 + tslib: 2.6.2 + dev: false + + /@aws-sdk/middleware-signing@3.391.0: + resolution: {integrity: sha512-2pAJJlZqaHc0d+cz2FTVrQmWi8ygKfqfczHUo/loCtOaMNtWXBHb/JsLEecs6cXdizy6gi3YsLz6VZYwY4Ssxw==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/types': 3.391.0 + '@smithy/property-provider': 2.0.16 + '@smithy/protocol-http': 2.0.5 + '@smithy/signature-v4': 2.0.18 + '@smithy/types': 2.7.0 + '@smithy/util-middleware': 2.0.8 + tslib: 2.6.2 + dev: false + + /@aws-sdk/middleware-stack@3.342.0: + resolution: {integrity: sha512-nDYtLAv9IZq8YFxtbyAiK/U1mtvtJS0DG6HiIPT5jpHcRpuWRHQ170EAW51zYts+21Ffj1VA6ZPkbup83+T6/w==} + engines: {node: '>=14.0.0'} + dependencies: + tslib: 2.6.2 + dev: false + + /@aws-sdk/middleware-user-agent@3.342.0: + resolution: {integrity: sha512-6iiFno+rq7W82mqM4KQKndIkZdGG1XZDlZIb77fcmQGYYlB1J2S/d0pIPdMk5ZQteuKJ5iorANUC0dKWw1mWTg==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/protocol-http': 3.342.0 + '@aws-sdk/types': 3.342.0 + '@aws-sdk/util-endpoints': 3.342.0 + tslib: 2.6.2 + dev: false + + /@aws-sdk/middleware-user-agent@3.391.0: + resolution: {integrity: sha512-LdK9uMNA14zqRw3B79Mhy7GX36qld/GYo93xuu+lr+AQ98leZEdc6GUbrtNDI3fP1Z8TMQcyHUKBml4/B+wXpQ==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/types': 3.391.0 + '@aws-sdk/util-endpoints': 3.391.0 + '@smithy/protocol-http': 2.0.5 + '@smithy/types': 2.7.0 + tslib: 2.6.2 + dev: false + + /@aws-sdk/node-config-provider@3.342.0: + resolution: {integrity: sha512-Mwkj4+zt64w7a8QDrI9q4SrEt7XRO30Vk0a0xENqcOGrKIPfF5aeqlw85NYLoGys+KV1oatqQ+k0GzKx8qTIdQ==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/property-provider': 3.342.0 + '@aws-sdk/shared-ini-file-loader': 3.342.0 + '@aws-sdk/types': 3.342.0 + tslib: 2.6.2 + dev: false + + /@aws-sdk/node-http-handler@3.342.0: + resolution: {integrity: sha512-ieNdrfJJMh46qY6rkV1azJBo3UfS9hc7d8CuHtkgHhCfH3BhxbtFqEiGilOdBmY5Sk69b//lFr4zHpUPYsXKaA==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/abort-controller': 3.342.0 + '@aws-sdk/protocol-http': 3.342.0 + '@aws-sdk/querystring-builder': 3.342.0 + '@aws-sdk/types': 3.342.0 + tslib: 2.6.2 + dev: false + + /@aws-sdk/property-provider@3.342.0: + resolution: {integrity: sha512-p4TR9yRakIpwupEH3BUijWMYThGG0q43n1ICcsBOcvWZpE636lIUw6nzFlOuBUwqyPfUyLbXzchvosYxfCl0jw==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/types': 3.342.0 + tslib: 2.6.2 + dev: false + + /@aws-sdk/protocol-http@3.342.0: + resolution: {integrity: sha512-zuF2urcTJBZ1tltPdTBQzRasuGB7+4Yfs9i5l0F7lE0luK5Azy6G+2r3WWENUNxFTYuP94GrrqaOhVyj8XXLPQ==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/types': 3.342.0 + tslib: 2.6.2 + dev: false + + /@aws-sdk/querystring-builder@3.342.0: + resolution: {integrity: sha512-tb3FbtC36a7XBYeupdKm60LeM0etp73I6/7pDAkzAlw7zJdvY0aQIvj1c0U6nZlwZF8sSSxC7vlamR+wCspdMw==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/types': 3.342.0 + '@aws-sdk/util-uri-escape': 3.310.0 + tslib: 2.6.2 + dev: false + + /@aws-sdk/querystring-parser@3.342.0: + resolution: {integrity: sha512-6svvr/LZW1EPJaARnOpjf92FIiK25wuO7fRq05gLTcTRAfUMDvub+oDg3Ro9EjJERumrYQrYCem5Qi4X9w8K2g==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/types': 3.342.0 + tslib: 2.6.2 + dev: false + + /@aws-sdk/service-error-classification@3.342.0: + resolution: {integrity: sha512-MwHO5McbdAVKxfQj1yhleboAXqrzcGoi9ODS+bwCwRfe2lakGzBBhu8zaGDlKYOdv5rS+yAPP/5fZZUiuZY8Bw==} + engines: {node: '>=14.0.0'} + dev: false + + /@aws-sdk/shared-ini-file-loader@3.342.0: + resolution: {integrity: sha512-kQG7TMQMhNp5+Y8vhGuO/+wU3K/dTx0xC0AKoDFiBf6EpDRmDfr2pPRnfJ9GwgS9haHxJ/3Uwc03swHMlsj20A==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/types': 3.342.0 + tslib: 2.6.2 + dev: false + + /@aws-sdk/signature-v4-multi-region@3.342.0: + resolution: {integrity: sha512-KjlkvzJ5bLDtnXYiYTSv4nLRqMFwhST3FCTx0uLm/Pbre2HnkrYRpG1nGND5f61jn4MetsNy5NlR+JEAORgbRQ==} + engines: {node: '>=14.0.0'} + peerDependencies: + '@aws-sdk/signature-v4-crt': ^3.118.0 + peerDependenciesMeta: + '@aws-sdk/signature-v4-crt': + optional: true + dependencies: + '@aws-sdk/protocol-http': 3.342.0 + '@aws-sdk/signature-v4': 3.342.0 + '@aws-sdk/types': 3.342.0 + tslib: 2.6.2 + dev: false + + /@aws-sdk/signature-v4@3.342.0: + resolution: {integrity: sha512-OWrGO2UOa1ENpy0kYd2shK4sklQygWUqvWLx9FotDbjIeUIEfAnqoPq/QqcXVrNyT/UvPi4iIrjHJEO8JCNRmA==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/eventstream-codec': 3.342.0 + '@aws-sdk/is-array-buffer': 3.310.0 + '@aws-sdk/types': 3.342.0 + '@aws-sdk/util-hex-encoding': 3.310.0 + '@aws-sdk/util-middleware': 3.342.0 + '@aws-sdk/util-uri-escape': 3.310.0 + '@aws-sdk/util-utf8': 3.310.0 + tslib: 2.6.2 + dev: false + + /@aws-sdk/smithy-client@3.342.0: + resolution: {integrity: sha512-HQ4JejjHU2X7OAZPwixFG+EyPSjmoZqll7EvWjPSKyclWrM320haWWz1trVzjG/AgPfeDLfRkH/JoMr13lECew==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/middleware-stack': 3.342.0 + '@aws-sdk/types': 3.342.0 + tslib: 2.6.2 + dev: false + + /@aws-sdk/token-providers@3.342.0: + resolution: {integrity: sha512-gYShxImNQVx3FYOUKB7nzzowYiiP1joyx43KrduHwBDV7hiqg7QhtJHr6Ek+QLPqcFKP9rRvo7NhGxu+T7dEQg==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/client-sso-oidc': 3.342.0 + '@aws-sdk/property-provider': 3.342.0 + '@aws-sdk/shared-ini-file-loader': 3.342.0 + '@aws-sdk/types': 3.342.0 + tslib: 2.6.2 + transitivePeerDependencies: + - aws-crt + dev: false + + /@aws-sdk/token-providers@3.391.0: + resolution: {integrity: sha512-kgfArsKLDJE71qQjfXiHiM5cZqgDHlMsqEx35+A65GmTWJaS1PGDqu3ZvVVU8E5mxnCCLw7vho21fsjvH6TBpg==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-crypto/sha256-browser': 3.0.0 + '@aws-crypto/sha256-js': 3.0.0 + '@aws-sdk/middleware-host-header': 3.391.0 + '@aws-sdk/middleware-logger': 3.391.0 + '@aws-sdk/middleware-recursion-detection': 3.391.0 + '@aws-sdk/middleware-user-agent': 3.391.0 + '@aws-sdk/types': 3.391.0 + '@aws-sdk/util-endpoints': 3.391.0 + '@aws-sdk/util-user-agent-browser': 3.391.0 + '@aws-sdk/util-user-agent-node': 3.391.0 + '@smithy/config-resolver': 2.0.21 + '@smithy/fetch-http-handler': 2.3.1 + '@smithy/hash-node': 2.0.17 + '@smithy/invalid-dependency': 2.0.15 + '@smithy/middleware-content-length': 2.0.17 + '@smithy/middleware-endpoint': 2.2.3 + '@smithy/middleware-retry': 2.0.25 + '@smithy/middleware-serde': 2.0.15 + '@smithy/middleware-stack': 2.0.9 + '@smithy/node-config-provider': 2.1.8 + '@smithy/node-http-handler': 2.2.1 + '@smithy/property-provider': 2.0.16 + '@smithy/protocol-http': 2.0.5 + '@smithy/shared-ini-file-loader': 2.2.7 + '@smithy/smithy-client': 2.2.0 + '@smithy/types': 2.7.0 + '@smithy/url-parser': 2.0.15 + '@smithy/util-base64': 2.0.1 + '@smithy/util-body-length-browser': 2.0.1 + '@smithy/util-body-length-node': 2.1.0 + '@smithy/util-defaults-mode-browser': 2.0.23 + '@smithy/util-defaults-mode-node': 2.0.30 + '@smithy/util-retry': 2.0.8 + '@smithy/util-utf8': 2.0.2 + tslib: 2.6.2 + transitivePeerDependencies: + - aws-crt + dev: false + + /@aws-sdk/types@3.342.0: + resolution: {integrity: sha512-5uyXVda/AgUpdZNJ9JPHxwyxr08miPiZ/CKSMcRdQVjcNnrdzY9m/iM9LvnQT44sQO+IEEkF2IoZIWvZcq199A==} + engines: {node: '>=14.0.0'} + dependencies: + tslib: 2.6.2 + dev: false + + /@aws-sdk/types@3.391.0: + resolution: {integrity: sha512-QpYVFKMOnzHz/JMj/b8wb18qxiT92U/5r5MmtRz2R3LOH6ooTO96k4ozXCrYr0qNed1PAnOj73rPrrH2wnCJKQ==} + engines: {node: '>=14.0.0'} + dependencies: + '@smithy/types': 2.7.0 + tslib: 2.6.2 + dev: false + + /@aws-sdk/url-parser@3.342.0: + resolution: {integrity: sha512-r4s/FDK6iywl8l4TqEwIwtNvxWO0kZes03c/yCiRYqxlkjVmbXEOodn5IAAweAeS9yqC3sl/wKbsaoBiGFn45g==} + dependencies: + '@aws-sdk/querystring-parser': 3.342.0 + '@aws-sdk/types': 3.342.0 + tslib: 2.6.2 + dev: false + + /@aws-sdk/util-base64@3.310.0: + resolution: {integrity: sha512-v3+HBKQvqgdzcbL+pFswlx5HQsd9L6ZTlyPVL2LS9nNXnCcR3XgGz9jRskikRUuUvUXtkSG1J88GAOnJ/apTPg==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/util-buffer-from': 3.310.0 + tslib: 2.6.2 + dev: false + + /@aws-sdk/util-body-length-browser@3.310.0: + resolution: {integrity: sha512-sxsC3lPBGfpHtNTUoGXMQXLwjmR0zVpx0rSvzTPAuoVILVsp5AU/w5FphNPxD5OVIjNbZv9KsKTuvNTiZjDp9g==} + dependencies: + tslib: 2.6.2 + dev: false + + /@aws-sdk/util-body-length-node@3.310.0: + resolution: {integrity: sha512-2tqGXdyKhyA6w4zz7UPoS8Ip+7sayOg9BwHNidiGm2ikbDxm1YrCfYXvCBdwaJxa4hJfRVz+aL9e+d3GqPI9pQ==} + engines: {node: '>=14.0.0'} + dependencies: + tslib: 2.6.2 + dev: false + + /@aws-sdk/util-buffer-from@3.310.0: + resolution: {integrity: sha512-i6LVeXFtGih5Zs8enLrt+ExXY92QV25jtEnTKHsmlFqFAuL3VBeod6boeMXkN2p9lbSVVQ1sAOOYZOHYbYkntw==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/is-array-buffer': 3.310.0 + tslib: 2.6.2 + dev: false + + /@aws-sdk/util-config-provider@3.310.0: + resolution: {integrity: sha512-xIBaYo8dwiojCw8vnUcIL4Z5tyfb1v3yjqyJKJWV/dqKUFOOS0U591plmXbM+M/QkXyML3ypon1f8+BoaDExrg==} + engines: {node: '>=14.0.0'} + dependencies: + tslib: 2.6.2 + dev: false + + /@aws-sdk/util-defaults-mode-browser@3.342.0: + resolution: {integrity: sha512-N1ZRvCLbrt4Re9MKU3pLYR0iO+H7GU7RsXG4yAq6DtSWT9WCw6xhIUpeV2T5uxWKL92o3WHNiGjwcebq+N73Bg==} + engines: {node: '>= 10.0.0'} + dependencies: + '@aws-sdk/property-provider': 3.342.0 + '@aws-sdk/types': 3.342.0 + bowser: 2.11.0 + tslib: 2.6.2 + dev: false + + /@aws-sdk/util-defaults-mode-node@3.342.0: + resolution: {integrity: sha512-yNa/eX8sELnwM5NONOFR/PCJMHTNrUVklSo/QHy57CT/L3KOqosRNAMnDVMzH1QolGaVN/8jgtDI2xVsvlP+AA==} + engines: {node: '>= 10.0.0'} + dependencies: + '@aws-sdk/config-resolver': 3.342.0 + '@aws-sdk/credential-provider-imds': 3.342.0 + '@aws-sdk/node-config-provider': 3.342.0 + '@aws-sdk/property-provider': 3.342.0 + '@aws-sdk/types': 3.342.0 + tslib: 2.6.2 + dev: false + + /@aws-sdk/util-dynamodb@3.391.0: + resolution: {integrity: sha512-cOqoIyWuQlLEhXorcnoq0P8mcN2iM/zyFRxazRyXYw3f6vjuDRGGAYhXHhcdkr+Nlr40pS7Al1559QktCbjb9g==} + engines: {node: '>=14.0.0'} + dependencies: + tslib: 2.6.2 + dev: false + + /@aws-sdk/util-endpoints@3.342.0: + resolution: {integrity: sha512-ZsYF413hkVwSOjvZG6U0SshRtzSg6MtwzO+j90AjpaqgoHAxE5LjO5eVYFfPXTC2U8NhU7xkzASY6++e5bRRnw==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/types': 3.342.0 + tslib: 2.6.2 + dev: false + + /@aws-sdk/util-endpoints@3.391.0: + resolution: {integrity: sha512-zv4sYDTQhNxyLoekcE02/nk3xvoo6yCHDy1kDJk0MFxOKaqUB+CvZdQBR4YBLSDlD4o4DUBmdYgKT58FfbM8sQ==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/types': 3.391.0 + tslib: 2.6.2 + dev: false + + /@aws-sdk/util-hex-encoding@3.310.0: + resolution: {integrity: sha512-sVN7mcCCDSJ67pI1ZMtk84SKGqyix6/0A1Ab163YKn+lFBQRMKexleZzpYzNGxYzmQS6VanP/cfU7NiLQOaSfA==} + engines: {node: '>=14.0.0'} + dependencies: + tslib: 2.6.2 + dev: false + + /@aws-sdk/util-locate-window@3.465.0: + resolution: {integrity: sha512-f+QNcWGswredzC1ExNAB/QzODlxwaTdXkNT5cvke2RLX8SFU5pYk6h4uCtWC0vWPELzOfMfloBrJefBzlarhsw==} + engines: {node: '>=14.0.0'} + dependencies: + tslib: 2.6.2 + dev: false + + /@aws-sdk/util-middleware@3.342.0: + resolution: {integrity: sha512-P2LYyMP4JUFZBy9DcMvCDxWU34mlShCyrqBZ1ouuGW7UMgRb1PTEvpLAVndIWn9H+1KGDFjMqOWp1FZHr4YZOA==} + engines: {node: '>=14.0.0'} + dependencies: + tslib: 2.6.2 + dev: false + + /@aws-sdk/util-retry@3.342.0: + resolution: {integrity: sha512-U1LXXtOMAQjU4H9gjYZng8auRponAH0t3vShHMKT8UQggT6Hwz1obdXUZgcLCtcjp/1aEK4MkDwk2JSjuUTaZw==} + engines: {node: '>= 14.0.0'} + dependencies: + '@aws-sdk/service-error-classification': 3.342.0 + tslib: 2.6.2 + dev: false + + /@aws-sdk/util-uri-escape@3.310.0: + resolution: {integrity: sha512-drzt+aB2qo2LgtDoiy/3sVG8w63cgLkqFIa2NFlGpUgHFWTXkqtbgf4L5QdjRGKWhmZsnqkbtL7vkSWEcYDJ4Q==} + engines: {node: '>=14.0.0'} + dependencies: + tslib: 2.6.2 + dev: false + + /@aws-sdk/util-user-agent-browser@3.342.0: + resolution: {integrity: sha512-FWHiBi1xaebzmq3LJsizgd2LCix/bKHUTOjTeO6hEYny5DyrOl0liwIA0mqgvfgwIoMOF/l6FGg7kTfKtNgkEA==} + dependencies: + '@aws-sdk/types': 3.342.0 + bowser: 2.11.0 + tslib: 2.6.2 + dev: false + + /@aws-sdk/util-user-agent-browser@3.391.0: + resolution: {integrity: sha512-6ipHOB1WdCBNeAMJauN7l2qNE0WLVaTNhkD290/ElXm1FHGTL8yw6lIDIjhIFO1bmbZxDiKApwDiG7ROhaJoxQ==} + dependencies: + '@aws-sdk/types': 3.391.0 + '@smithy/types': 2.7.0 + bowser: 2.11.0 + tslib: 2.6.2 + dev: false + + /@aws-sdk/util-user-agent-node@3.342.0: + resolution: {integrity: sha512-YMAhUar4CAB6hfUR72FH0sRqMBhPajDIhiKrZEOy7+qaWFdfb/t9DYi6p3PYIUZWK2vkESiDoX9Ays2xsp9rOQ==} + engines: {node: '>=14.0.0'} + peerDependencies: + aws-crt: '>=1.0.0' + peerDependenciesMeta: + aws-crt: + optional: true + dependencies: + '@aws-sdk/node-config-provider': 3.342.0 + '@aws-sdk/types': 3.342.0 + tslib: 2.6.2 + dev: false + + /@aws-sdk/util-user-agent-node@3.391.0: + resolution: {integrity: sha512-PVvAK/Lf4BdB1eJIZtyFpGSslGQwKpYt9/hKs5NlR+qxBMXU9T0DnTqH4GiXZaazvXr7OUVWitIF2b7iKBMTow==} + engines: {node: '>=14.0.0'} + peerDependencies: + aws-crt: '>=1.0.0' + peerDependenciesMeta: + aws-crt: + optional: true + dependencies: + '@aws-sdk/types': 3.391.0 + '@smithy/node-config-provider': 2.1.8 + '@smithy/types': 2.7.0 + tslib: 2.6.2 + dev: false + + /@aws-sdk/util-utf8-browser@3.259.0: + resolution: {integrity: sha512-UvFa/vR+e19XookZF8RzFZBrw2EUkQWxiBW0yYQAhvk3C+QVGl0H3ouca8LDBlBfQKXwmW3huo/59H8rwb1wJw==} + dependencies: + tslib: 2.6.2 + dev: false + + /@aws-sdk/util-utf8@3.310.0: + resolution: {integrity: sha512-DnLfFT8uCO22uOJc0pt0DsSNau1GTisngBCDw8jQuWT5CqogMJu4b/uXmwEqfj8B3GX6Xsz8zOd6JpRlPftQoA==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/util-buffer-from': 3.310.0 + tslib: 2.6.2 + dev: false + + /@babel/code-frame@7.23.5: + resolution: {integrity: sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/highlight': 7.23.4 + chalk: 2.4.2 + + /@babel/compat-data@7.23.5: + resolution: {integrity: sha512-uU27kfDRlhfKl+w1U6vp16IuvSLtjAxdArVXPa9BvLkrr7CYIsxH5adpHObeAGY/41+syctUWOZ140a2Rvkgjw==} + engines: {node: '>=6.9.0'} + + /@babel/core@7.23.6: + resolution: {integrity: sha512-FxpRyGjrMJXh7X3wGLGhNDCRiwpWEF74sKjTLDJSG5Kyvow3QZaG0Adbqzi9ZrVjTWpsX+2cxWXD71NMg93kdw==} + engines: {node: '>=6.9.0'} + dependencies: + '@ampproject/remapping': 2.2.1 + '@babel/code-frame': 7.23.5 + '@babel/generator': 7.23.6 + '@babel/helper-compilation-targets': 7.23.6 + '@babel/helper-module-transforms': 7.23.3(@babel/core@7.23.6) + '@babel/helpers': 7.23.6 + '@babel/parser': 7.23.6 + '@babel/template': 7.22.15 + '@babel/traverse': 7.23.6 + '@babel/types': 7.23.6 + convert-source-map: 2.0.0 + debug: 4.3.4 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + /@babel/core@7.23.7: + resolution: {integrity: sha512-+UpDgowcmqe36d4NwqvKsyPMlOLNGMsfMmQ5WGCu+siCe3t3dfe9njrzGfdN4qq+bcNUt0+Vw6haRxBOycs4dw==} + engines: {node: '>=6.9.0'} + dependencies: + '@ampproject/remapping': 2.2.1 + '@babel/code-frame': 7.23.5 + '@babel/generator': 7.23.6 + '@babel/helper-compilation-targets': 7.23.6 + '@babel/helper-module-transforms': 7.23.3(@babel/core@7.23.7) + '@babel/helpers': 7.23.7 + '@babel/parser': 7.23.6 + '@babel/template': 7.22.15 + '@babel/traverse': 7.23.7 + '@babel/types': 7.23.6 + convert-source-map: 2.0.0 + debug: 4.3.4 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + /@babel/eslint-parser@7.23.3(@babel/core@7.23.6)(eslint@8.56.0): + resolution: {integrity: sha512-9bTuNlyx7oSstodm1cR1bECj4fkiknsDa1YniISkJemMY3DGhJNYBECbe6QD/q54mp2J8VO66jW3/7uP//iFCw==} + engines: {node: ^10.13.0 || ^12.13.0 || >=14.0.0} + peerDependencies: + '@babel/core': ^7.11.0 + eslint: ^7.5.0 || ^8.0.0 + dependencies: + '@babel/core': 7.23.6 + '@nicolo-ribaudo/eslint-scope-5-internals': 5.1.1-v1 + eslint: 8.56.0 + eslint-visitor-keys: 2.1.0 + semver: 6.3.1 + dev: true + + /@babel/generator@7.23.6: + resolution: {integrity: sha512-qrSfCYxYQB5owCmGLbl8XRpX1ytXlpueOb0N0UmQwA073KZxejgQTzAmJezxvpwQD9uGtK2shHdi55QT+MbjIw==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/types': 7.23.6 + '@jridgewell/gen-mapping': 0.3.3 + '@jridgewell/trace-mapping': 0.3.20 + jsesc: 2.5.2 + + /@babel/helper-compilation-targets@7.23.6: + resolution: {integrity: sha512-9JB548GZoQVmzrFgp8o7KxdgkTGm6xs9DW0o/Pim72UDjzr5ObUQ6ZzYPqA+g9OTS2bBQoctLJrky0RDCAWRgQ==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/compat-data': 7.23.5 + '@babel/helper-validator-option': 7.23.5 + browserslist: 4.22.2 + lru-cache: 5.1.1 + semver: 6.3.1 + + /@babel/helper-environment-visitor@7.22.20: + resolution: {integrity: sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==} + engines: {node: '>=6.9.0'} + + /@babel/helper-function-name@7.23.0: + resolution: {integrity: sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/template': 7.22.15 + '@babel/types': 7.23.6 + + /@babel/helper-hoist-variables@7.22.5: + resolution: {integrity: sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/types': 7.23.6 + + /@babel/helper-module-imports@7.22.15: + resolution: {integrity: sha512-0pYVBnDKZO2fnSPCrgM/6WMc7eS20Fbok+0r88fp+YtWVLZrp4CkafFGIp+W0VKw4a22sgebPT99y+FDNMdP4w==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/types': 7.23.6 + + /@babel/helper-module-transforms@7.23.3(@babel/core@7.23.6): + resolution: {integrity: sha512-7bBs4ED9OmswdfDzpz4MpWgSrV7FXlc3zIagvLFjS5H+Mk7Snr21vQ6QwrsoCGMfNC4e4LQPdoULEt4ykz0SRQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.23.6 + '@babel/helper-environment-visitor': 7.22.20 + '@babel/helper-module-imports': 7.22.15 + '@babel/helper-simple-access': 7.22.5 + '@babel/helper-split-export-declaration': 7.22.6 + '@babel/helper-validator-identifier': 7.22.20 + + /@babel/helper-module-transforms@7.23.3(@babel/core@7.23.7): + resolution: {integrity: sha512-7bBs4ED9OmswdfDzpz4MpWgSrV7FXlc3zIagvLFjS5H+Mk7Snr21vQ6QwrsoCGMfNC4e4LQPdoULEt4ykz0SRQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.23.7 + '@babel/helper-environment-visitor': 7.22.20 + '@babel/helper-module-imports': 7.22.15 + '@babel/helper-simple-access': 7.22.5 + '@babel/helper-split-export-declaration': 7.22.6 + '@babel/helper-validator-identifier': 7.22.20 + + /@babel/helper-plugin-utils@7.22.5: + resolution: {integrity: sha512-uLls06UVKgFG9QD4OeFYLEGteMIAa5kpTPcFL28yuCIIzsf6ZyKZMllKVOCZFhiZ5ptnwX4mtKdWCBE/uT4amg==} + engines: {node: '>=6.9.0'} + + /@babel/helper-simple-access@7.22.5: + resolution: {integrity: sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/types': 7.23.6 + + /@babel/helper-split-export-declaration@7.22.6: + resolution: {integrity: sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/types': 7.23.6 + + /@babel/helper-string-parser@7.23.4: + resolution: {integrity: sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ==} + engines: {node: '>=6.9.0'} + + /@babel/helper-validator-identifier@7.22.20: + resolution: {integrity: sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==} + engines: {node: '>=6.9.0'} + + /@babel/helper-validator-option@7.23.5: + resolution: {integrity: sha512-85ttAOMLsr53VgXkTbkx8oA6YTfT4q7/HzXSLEYmjcSTJPMPQtvq1BD79Byep5xMUYbGRzEpDsjUf3dyp54IKw==} + engines: {node: '>=6.9.0'} + + /@babel/helpers@7.23.6: + resolution: {integrity: sha512-wCfsbN4nBidDRhpDhvcKlzHWCTlgJYUUdSJfzXb2NuBssDSIjc3xcb+znA7l+zYsFljAcGM0aFkN40cR3lXiGA==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/template': 7.22.15 + '@babel/traverse': 7.23.6 + '@babel/types': 7.23.6 + transitivePeerDependencies: + - supports-color + + /@babel/helpers@7.23.7: + resolution: {integrity: sha512-6AMnjCoC8wjqBzDHkuqpa7jAKwvMo4dC+lr/TFBz+ucfulO1XMpDnwWPGBNwClOKZ8h6xn5N81W/R5OrcKtCbQ==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/template': 7.22.15 + '@babel/traverse': 7.23.7 + '@babel/types': 7.23.6 + transitivePeerDependencies: + - supports-color + + /@babel/highlight@7.23.4: + resolution: {integrity: sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/helper-validator-identifier': 7.22.20 + chalk: 2.4.2 + js-tokens: 4.0.0 + + /@babel/parser@7.23.6: + resolution: {integrity: sha512-Z2uID7YJ7oNvAI20O9X0bblw7Qqs8Q2hFy0R9tAfnfLkp5MW0UH9eUvnDSnFwKZ0AvgS1ucqR4KzvVHgnke1VQ==} + engines: {node: '>=6.0.0'} + hasBin: true + dependencies: + '@babel/types': 7.23.6 + + /@babel/plugin-syntax-async-generators@7.8.4(@babel/core@7.23.6): + resolution: {integrity: sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.6 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/plugin-syntax-async-generators@7.8.4(@babel/core@7.23.7): + resolution: {integrity: sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.7 + '@babel/helper-plugin-utils': 7.22.5 + + /@babel/plugin-syntax-bigint@7.8.3(@babel/core@7.23.6): + resolution: {integrity: sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.6 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/plugin-syntax-bigint@7.8.3(@babel/core@7.23.7): + resolution: {integrity: sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.7 + '@babel/helper-plugin-utils': 7.22.5 + + /@babel/plugin-syntax-class-properties@7.12.13(@babel/core@7.23.6): + resolution: {integrity: sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.6 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/plugin-syntax-class-properties@7.12.13(@babel/core@7.23.7): + resolution: {integrity: sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.7 + '@babel/helper-plugin-utils': 7.22.5 + + /@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.23.6): + resolution: {integrity: sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.6 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.23.7): + resolution: {integrity: sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.7 + '@babel/helper-plugin-utils': 7.22.5 + + /@babel/plugin-syntax-json-strings@7.8.3(@babel/core@7.23.6): + resolution: {integrity: sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.6 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/plugin-syntax-json-strings@7.8.3(@babel/core@7.23.7): + resolution: {integrity: sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.7 + '@babel/helper-plugin-utils': 7.22.5 + + /@babel/plugin-syntax-jsx@7.23.3(@babel/core@7.23.7): + resolution: {integrity: sha512-EB2MELswq55OHUoRZLGg/zC7QWUKfNLpE57m/S2yr1uEneIgsTgrSzXP3NXEsMkVn76OlaVVnzN+ugObuYGwhg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.7 + '@babel/helper-plugin-utils': 7.22.5 + + /@babel/plugin-syntax-logical-assignment-operators@7.10.4(@babel/core@7.23.6): + resolution: {integrity: sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.6 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/plugin-syntax-logical-assignment-operators@7.10.4(@babel/core@7.23.7): + resolution: {integrity: sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.7 + '@babel/helper-plugin-utils': 7.22.5 + + /@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.23.6): + resolution: {integrity: sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.6 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.23.7): + resolution: {integrity: sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.7 + '@babel/helper-plugin-utils': 7.22.5 + + /@babel/plugin-syntax-numeric-separator@7.10.4(@babel/core@7.23.6): + resolution: {integrity: sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.6 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/plugin-syntax-numeric-separator@7.10.4(@babel/core@7.23.7): + resolution: {integrity: sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.7 + '@babel/helper-plugin-utils': 7.22.5 + + /@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.23.6): + resolution: {integrity: sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.6 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.23.7): + resolution: {integrity: sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.7 + '@babel/helper-plugin-utils': 7.22.5 + + /@babel/plugin-syntax-optional-catch-binding@7.8.3(@babel/core@7.23.6): + resolution: {integrity: sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.6 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/plugin-syntax-optional-catch-binding@7.8.3(@babel/core@7.23.7): + resolution: {integrity: sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.7 + '@babel/helper-plugin-utils': 7.22.5 + + /@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.23.6): + resolution: {integrity: sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.6 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.23.7): + resolution: {integrity: sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.7 + '@babel/helper-plugin-utils': 7.22.5 + + /@babel/plugin-syntax-top-level-await@7.14.5(@babel/core@7.23.6): + resolution: {integrity: sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.6 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/plugin-syntax-top-level-await@7.14.5(@babel/core@7.23.7): + resolution: {integrity: sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.7 + '@babel/helper-plugin-utils': 7.22.5 + + /@babel/plugin-syntax-typescript@7.23.3(@babel/core@7.23.6): + resolution: {integrity: sha512-9EiNjVJOMwCO+43TqoTrgQ8jMwcAd0sWyXi9RPfIsLTj4R2MADDDQXELhffaUx/uJv2AYcxBgPwH6j4TIA4ytQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.6 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/plugin-syntax-typescript@7.23.3(@babel/core@7.23.7): + resolution: {integrity: sha512-9EiNjVJOMwCO+43TqoTrgQ8jMwcAd0sWyXi9RPfIsLTj4R2MADDDQXELhffaUx/uJv2AYcxBgPwH6j4TIA4ytQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.7 + '@babel/helper-plugin-utils': 7.22.5 + + /@babel/runtime@7.23.6: + resolution: {integrity: sha512-zHd0eUrf5GZoOWVCXp6koAKQTfZV07eit6bGPmJgnZdnSAvvZee6zniW2XMF7Cmc4ISOOnPy3QaSiIJGJkVEDQ==} + engines: {node: '>=6.9.0'} + dependencies: + regenerator-runtime: 0.14.1 + dev: true + + /@babel/template@7.22.15: + resolution: {integrity: sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/code-frame': 7.23.5 + '@babel/parser': 7.23.6 + '@babel/types': 7.23.6 + + /@babel/traverse@7.23.6: + resolution: {integrity: sha512-czastdK1e8YByZqezMPFiZ8ahwVMh/ESl9vPgvgdB9AmFMGP5jfpFax74AQgl5zj4XHzqeYAg2l8PuUeRS1MgQ==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/code-frame': 7.23.5 + '@babel/generator': 7.23.6 + '@babel/helper-environment-visitor': 7.22.20 + '@babel/helper-function-name': 7.23.0 + '@babel/helper-hoist-variables': 7.22.5 + '@babel/helper-split-export-declaration': 7.22.6 + '@babel/parser': 7.23.6 + '@babel/types': 7.23.6 + debug: 4.3.4 + globals: 11.12.0 + transitivePeerDependencies: + - supports-color + + /@babel/traverse@7.23.7: + resolution: {integrity: sha512-tY3mM8rH9jM0YHFGyfC0/xf+SB5eKUu7HPj7/k3fpi9dAlsMc5YbQvDi0Sh2QTPXqMhyaAtzAr807TIyfQrmyg==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/code-frame': 7.23.5 + '@babel/generator': 7.23.6 + '@babel/helper-environment-visitor': 7.22.20 + '@babel/helper-function-name': 7.23.0 + '@babel/helper-hoist-variables': 7.22.5 + '@babel/helper-split-export-declaration': 7.22.6 + '@babel/parser': 7.23.6 + '@babel/types': 7.23.6 + debug: 4.3.4 + globals: 11.12.0 + transitivePeerDependencies: + - supports-color + + /@babel/types@7.23.6: + resolution: {integrity: sha512-+uarb83brBzPKN38NX1MkB6vb6+mwvR6amUulqAE7ccQw1pEl+bCia9TbdG1lsnFP7lZySvUn37CHyXQdfTwzg==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/helper-string-parser': 7.23.4 + '@babel/helper-validator-identifier': 7.22.20 + to-fast-properties: 2.0.0 + + /@bcoe/v8-coverage@0.2.3: + resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==} + + /@cdktf/cli-core@0.20.0-pre.70(react@17.0.2): + resolution: {integrity: sha512-oK/agnl4Kd1zAbhwrSi8lhcqzm10wsxz06iqBgpcAEmGluTeJqf+Uso57AHDu/sJfSZdiKvApJ1liExZiJ+h6A==} + dependencies: + '@cdktf/commons': 0.20.0-pre.70(constructs@10.3.0) + '@cdktf/hcl2cdk': 0.20.0-pre.70(constructs@10.3.0) + '@cdktf/hcl2json': 0.20.0-pre.70 + '@cdktf/node-pty-prebuilt-multiarch': 0.10.1-pre.11 + '@cdktf/provider-schema': 0.20.0-pre.70(constructs@10.3.0) + '@sentry/node': 7.92.0 + archiver: 5.3.2 + cdktf: 0.20.0-pre.70(constructs@10.3.0) + chalk: 4.1.2 + chokidar: 3.5.3 + cli-spinners: 2.9.1 + codemaker: 1.94.0 + constructs: 10.3.0 + cross-fetch: 3.1.8 + cross-spawn: 7.0.3 + detect-port: 1.5.1 + execa: 5.1.1 + extract-zip: 2.0.1 + follow-redirects: 1.15.4 + fs-extra: 8.1.0 + https-proxy-agent: 5.0.1 + indent-string: 4.0.0 + ink: 3.2.0(react@17.0.2) + ink-select-input: 4.2.2(ink@3.2.0)(react@17.0.2) + ink-spinner: 4.0.3(ink@3.2.0)(react@17.0.2) + ink-testing-library: 2.1.0 + ink-use-stdout-dimensions: 1.0.5(ink@3.2.0)(react@17.0.2) + jsii: 5.3.3 + jsii-pacmak: 1.94.0 + jsii-srcmak: 0.1.1000 + lodash.isequal: 4.5.0 + log4js: 6.9.1 + minimatch: 5.1.6 + node-fetch: 2.7.0 + open: 7.4.2 + parse-gitignore: 1.0.1 + pkg-up: 3.1.0 + semver: 7.5.4 + sscaff: 1.2.274 + stream-buffers: 3.0.2 + strip-ansi: 6.0.1 + tunnel-agent: 0.6.0 + uuid: 8.3.2 + xml-js: 1.6.11 + xstate: 4.38.3 + yargs: 17.7.2 + yoga-layout-prebuilt: 1.10.0 + zod: 3.22.4 + transitivePeerDependencies: + - '@types/react' + - bufferutil + - debug + - encoding + - react + - supports-color + - utf-8-validate + dev: false + + /@cdktf/commons@0.20.0-pre.70(constructs@10.3.0): + resolution: {integrity: sha512-5D5PvnmBUyJN1oN3D75oaIaOCH5o1Qkq376PPFuiCDzFPsMdjP9KMuOVJwWDcpX+LxbqcFZG1KFIeEgSeHQ+MA==} + dependencies: + '@sentry/node': 7.92.0 + cdktf: 0.20.0-pre.70(constructs@10.3.0) + ci-info: 3.9.0 + codemaker: 1.94.0 + cross-spawn: 7.0.3 + follow-redirects: 1.15.4 + fs-extra: 11.2.0 + is-valid-domain: 0.1.6 + log4js: 6.9.1 + strip-ansi: 6.0.1 + uuid: 9.0.1 + transitivePeerDependencies: + - constructs + - debug + - supports-color + dev: false + + /@cdktf/hcl2cdk@0.20.0-pre.70(constructs@10.3.0): + resolution: {integrity: sha512-kHVs7RHpVGBsilx4UJfr/AI/6GlAhQTtOhauCXjr0eqKEGNrmxnP+HAOxzfsQPb+gsr/S+7yYtC/p/BWF0oZlQ==} + dependencies: + '@babel/generator': 7.23.6 + '@babel/template': 7.22.15 + '@babel/types': 7.23.6 + '@cdktf/commons': 0.20.0-pre.70(constructs@10.3.0) + '@cdktf/hcl2json': 0.20.0-pre.70 + '@cdktf/provider-generator': 0.20.0-pre.70(constructs@10.3.0) + '@cdktf/provider-schema': 0.20.0-pre.70(constructs@10.3.0) + camelcase: 6.3.0 + cdktf: 0.20.0-pre.70(constructs@10.3.0) + codemaker: 1.94.0 + deep-equal: 2.2.3 + glob: 10.3.10 + graphology: 0.25.4(graphology-types@0.24.7) + graphology-types: 0.24.7 + jsii-rosetta: 5.3.3 + prettier: 2.8.8 + reserved-words: 0.1.2 + zod: 3.22.4 + transitivePeerDependencies: + - constructs + - debug + - supports-color + dev: false + + /@cdktf/hcl2json@0.20.0-pre.70: + resolution: {integrity: sha512-lSK4i+cm2TbTeIs86ga08HcTPNkk8ixMnlrjcqUOsWhrG87hHZG/kwpaepfDZ9su7sCXHMEEOStSKOb7Ve+foQ==} + dependencies: + fs-extra: 11.2.0 + dev: false + + /@cdktf/node-pty-prebuilt-multiarch@0.10.1-pre.11: + resolution: {integrity: sha512-qvga/nzEtdCJMu/6jJfDqpzbRejvXtNhWFnbubfuYyN5nMNORNXX+POT4j+mQSDQar5bIQ1a812szw/zr47cfw==} + requiresBuild: true + dependencies: + nan: 2.18.0 + prebuild-install: 7.1.1 + dev: false + + /@cdktf/provider-archive@9.0.1(cdktf@0.20.0-pre.70)(constructs@10.3.0): + resolution: {integrity: sha512-NEFw9c6PE3VDptYFCoaCY9aVL3841NpT0kBGc4XRsU6wACvqnPxqLL/xUzFYZz4PtBagpGPVQTCIdUdalsEb7g==} + engines: {node: '>= 18.12.0'} + peerDependencies: + cdktf: ^0.19.0 + constructs: ^10.3.0 + dependencies: + cdktf: 0.20.0-pre.70(constructs@10.3.0) + constructs: 10.3.0 + dev: false + + /@cdktf/provider-aws@18.2.0(cdktf@0.20.0-pre.70)(constructs@10.3.0): + resolution: {integrity: sha512-Dm+ORqd9ttGH8PqdDe4q72QHoHO0BfgBEMDasagcP4b0lhYV3oiYxJtHsqdOJEKiv6EvHZT3/iftXwU2uOjnlA==} + engines: {node: '>= 18.12.0'} + peerDependencies: + cdktf: ^0.19.0 + constructs: ^10.3.0 + dependencies: + cdktf: 0.20.0-pre.70(constructs@10.3.0) + constructs: 10.3.0 + dev: false + + /@cdktf/provider-generator@0.20.0-pre.70(constructs@10.3.0): + resolution: {integrity: sha512-g5/N6HROmdd5voVO0cq2Fy5YrYOJ3dWxbgECET76KLkgBjEuWb0jFtFkUejZnds6eXKO8fmZJ+Waz/DQPncqxA==} + dependencies: + '@cdktf/commons': 0.20.0-pre.70(constructs@10.3.0) + '@cdktf/provider-schema': 0.20.0-pre.70(constructs@10.3.0) + '@types/node': 18.19.4 + codemaker: 1.94.0 + fs-extra: 8.1.0 + glob: 10.3.10 + jsii-srcmak: 0.1.1000 + transitivePeerDependencies: + - constructs + - debug + - supports-color + dev: false + + /@cdktf/provider-local@9.0.1(cdktf@0.20.0-pre.70)(constructs@10.3.0): + resolution: {integrity: sha512-8CEbPmTfqXYLOVbTMqPtniWlKe+VyeuTsVEXvdHVUmn/WREs2wsYwrQ2DEeekGwrkgI2CL9n17HFI6CyZdMtiQ==} + engines: {node: '>= 18.12.0'} + peerDependencies: + cdktf: ^0.19.0 + constructs: ^10.3.0 + dependencies: + cdktf: 0.20.0-pre.70(constructs@10.3.0) + constructs: 10.3.0 + dev: false + + /@cdktf/provider-newrelic@11.0.5(cdktf@0.20.0-pre.70)(constructs@10.3.0): + resolution: {integrity: sha512-DXTzR6bjqnA777YhzQ1ZRUKCMPxxVArZAQTWtiaAKOrDwiKqhoj15y+Yn5V5nMZUrgcqub7vCK01hXyKqT2DKA==} + engines: {node: '>= 18.12.0'} + deprecated: See https://cdk.tf/imports for details on how to continue to use the newrelic provider in your CDK for Terraform (CDKTF) projects by generating the bindings locally. + peerDependencies: + cdktf: ^0.19.0 + constructs: ^10.3.0 + dependencies: + cdktf: 0.20.0-pre.70(constructs@10.3.0) + constructs: 10.3.0 + dev: false + + /@cdktf/provider-null@9.0.1(cdktf@0.20.0-pre.70)(constructs@10.3.0): + resolution: {integrity: sha512-unmz1i944Y0gIavZTYl54jZBXQh95i21SStCRqRfiFyrsc88aw665HJt1G69uQXK3VtPuwjFSSVsG9UMRmZh6A==} + engines: {node: '>= 18.12.0'} + peerDependencies: + cdktf: ^0.19.0 + constructs: ^10.0.0 + dependencies: + cdktf: 0.20.0-pre.70(constructs@10.3.0) + constructs: 10.3.0 + dev: false + + /@cdktf/provider-pagerduty@12.2.0(cdktf@0.20.0-pre.70)(constructs@10.3.0): + resolution: {integrity: sha512-/jTC4OdtaBRG4fTiA8kMlmYcw/t6VMeaC1CsJjNWZgSoEVfZBwP+RDFpq+mjQ1YCWEcCES93zB9L23ql+XIUPg==} + engines: {node: '>= 18.12.0'} + deprecated: See https://cdk.tf/imports for details on how to continue to use the pagerduty provider in your CDK for Terraform (CDKTF) projects by generating the bindings locally. + peerDependencies: + cdktf: ^0.19.0 + constructs: ^10.3.0 + dependencies: + cdktf: 0.20.0-pre.70(constructs@10.3.0) + constructs: 10.3.0 + dev: false + + /@cdktf/provider-schema@0.20.0-pre.70(constructs@10.3.0): + resolution: {integrity: sha512-in1XqvD0MxJMVFL8bUeA1OPpQXlhEP2sLMKXTXrossZcCxDCpfdSJOyB0b/Ybr/cET+hcJVdBDlEoSHYIdJLDQ==} + dependencies: + '@cdktf/commons': 0.20.0-pre.70(constructs@10.3.0) + '@cdktf/hcl2json': 0.20.0-pre.70 + deepmerge: 4.3.1 + fs-extra: 11.2.0 + transitivePeerDependencies: + - constructs + - debug + - supports-color + dev: false + + /@cdktf/provider-time@9.0.2(cdktf@0.20.0-pre.70)(constructs@10.3.0): + resolution: {integrity: sha512-I0BS+/Gs/2fWXqGcmDsUvWqiCXDYcYH0OKvVvUf1RrGc8678uCeyqVqnwdy+UkIwjUGwk3L9pfJKX3dsun8OUQ==} + engines: {node: '>= 18.12.0'} + peerDependencies: + cdktf: ^0.19.0 + constructs: ^10.3.0 + dependencies: + cdktf: 0.20.0-pre.70(constructs@10.3.0) + constructs: 10.3.0 + dev: false + + /@colors/colors@1.5.0: + resolution: {integrity: sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==} + engines: {node: '>=0.1.90'} + requiresBuild: true + dev: false + optional: true + + /@colors/colors@1.6.0: + resolution: {integrity: sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==} + engines: {node: '>=0.1.90'} + dev: false + + /@cspotcode/source-map-support@0.8.1: + resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} + engines: {node: '>=12'} + dependencies: + '@jridgewell/trace-mapping': 0.3.9 + + /@dabh/diagnostics@2.0.3: + resolution: {integrity: sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==} + dependencies: + colorspace: 1.1.4 + enabled: 2.0.0 + kuler: 2.0.0 + dev: false + + /@esbuild/aix-ppc64@0.19.11: + resolution: {integrity: sha512-FnzU0LyE3ySQk7UntJO4+qIiQgI7KoODnZg5xzXIrFJlKd2P2gwHsHY4927xj9y5PJmJSzULiUCWmv7iWnNa7g==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [aix] + requiresBuild: true + dev: false + optional: true + + /@esbuild/android-arm64@0.19.11: + resolution: {integrity: sha512-aiu7K/5JnLj//KOnOfEZ0D90obUkRzDMyqd/wNAUQ34m4YUPVhRZpnqKV9uqDGxT7cToSDnIHsGooyIczu9T+Q==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + requiresBuild: true + dev: false + optional: true + + /@esbuild/android-arm@0.19.11: + resolution: {integrity: sha512-5OVapq0ClabvKvQ58Bws8+wkLCV+Rxg7tUVbo9xu034Nm536QTII4YzhaFriQ7rMrorfnFKUsArD2lqKbFY4vw==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + requiresBuild: true + dev: false + optional: true + + /@esbuild/android-x64@0.19.11: + resolution: {integrity: sha512-eccxjlfGw43WYoY9QgB82SgGgDbibcqyDTlk3l3C0jOVHKxrjdc9CTwDUQd0vkvYg5um0OH+GpxYvp39r+IPOg==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + requiresBuild: true + dev: false + optional: true + + /@esbuild/darwin-arm64@0.19.11: + resolution: {integrity: sha512-ETp87DRWuSt9KdDVkqSoKoLFHYTrkyz2+65fj9nfXsaV3bMhTCjtQfw3y+um88vGRKRiF7erPrh/ZuIdLUIVxQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + requiresBuild: true + dev: false + optional: true + + /@esbuild/darwin-x64@0.19.11: + resolution: {integrity: sha512-fkFUiS6IUK9WYUO/+22omwetaSNl5/A8giXvQlcinLIjVkxwTLSktbF5f/kJMftM2MJp9+fXqZ5ezS7+SALp4g==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + requiresBuild: true + dev: false + optional: true + + /@esbuild/freebsd-arm64@0.19.11: + resolution: {integrity: sha512-lhoSp5K6bxKRNdXUtHoNc5HhbXVCS8V0iZmDvyWvYq9S5WSfTIHU2UGjcGt7UeS6iEYp9eeymIl5mJBn0yiuxA==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + requiresBuild: true + dev: false + optional: true + + /@esbuild/freebsd-x64@0.19.11: + resolution: {integrity: sha512-JkUqn44AffGXitVI6/AbQdoYAq0TEullFdqcMY/PCUZ36xJ9ZJRtQabzMA+Vi7r78+25ZIBosLTOKnUXBSi1Kw==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + requiresBuild: true + dev: false + optional: true + + /@esbuild/linux-arm64@0.19.11: + resolution: {integrity: sha512-LneLg3ypEeveBSMuoa0kwMpCGmpu8XQUh+mL8XXwoYZ6Be2qBnVtcDI5azSvh7vioMDhoJFZzp9GWp9IWpYoUg==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: false + optional: true + + /@esbuild/linux-arm@0.19.11: + resolution: {integrity: sha512-3CRkr9+vCV2XJbjwgzjPtO8T0SZUmRZla+UL1jw+XqHZPkPgZiyWvbDvl9rqAN8Zl7qJF0O/9ycMtjU67HN9/Q==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + requiresBuild: true + dev: false + optional: true + + /@esbuild/linux-ia32@0.19.11: + resolution: {integrity: sha512-caHy++CsD8Bgq2V5CodbJjFPEiDPq8JJmBdeyZ8GWVQMjRD0sU548nNdwPNvKjVpamYYVL40AORekgfIubwHoA==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + requiresBuild: true + dev: false + optional: true + + /@esbuild/linux-loong64@0.19.11: + resolution: {integrity: sha512-ppZSSLVpPrwHccvC6nQVZaSHlFsvCQyjnvirnVjbKSHuE5N24Yl8F3UwYUUR1UEPaFObGD2tSvVKbvR+uT1Nrg==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + requiresBuild: true + dev: false + optional: true + + /@esbuild/linux-mips64el@0.19.11: + resolution: {integrity: sha512-B5x9j0OgjG+v1dF2DkH34lr+7Gmv0kzX6/V0afF41FkPMMqaQ77pH7CrhWeR22aEeHKaeZVtZ6yFwlxOKPVFyg==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + requiresBuild: true + dev: false + optional: true + + /@esbuild/linux-ppc64@0.19.11: + resolution: {integrity: sha512-MHrZYLeCG8vXblMetWyttkdVRjQlQUb/oMgBNurVEnhj4YWOr4G5lmBfZjHYQHHN0g6yDmCAQRR8MUHldvvRDA==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + requiresBuild: true + dev: false + optional: true + + /@esbuild/linux-riscv64@0.19.11: + resolution: {integrity: sha512-f3DY++t94uVg141dozDu4CCUkYW+09rWtaWfnb3bqe4w5NqmZd6nPVBm+qbz7WaHZCoqXqHz5p6CM6qv3qnSSQ==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + requiresBuild: true + dev: false + optional: true + + /@esbuild/linux-s390x@0.19.11: + resolution: {integrity: sha512-A5xdUoyWJHMMlcSMcPGVLzYzpcY8QP1RtYzX5/bS4dvjBGVxdhuiYyFwp7z74ocV7WDc0n1harxmpq2ePOjI0Q==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + requiresBuild: true + dev: false + optional: true + + /@esbuild/linux-x64@0.19.11: + resolution: {integrity: sha512-grbyMlVCvJSfxFQUndw5mCtWs5LO1gUlwP4CDi4iJBbVpZcqLVT29FxgGuBJGSzyOxotFG4LoO5X+M1350zmPA==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: false + optional: true + + /@esbuild/netbsd-x64@0.19.11: + resolution: {integrity: sha512-13jvrQZJc3P230OhU8xgwUnDeuC/9egsjTkXN49b3GcS5BKvJqZn86aGM8W9pd14Kd+u7HuFBMVtrNGhh6fHEQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + requiresBuild: true + dev: false + optional: true + + /@esbuild/openbsd-x64@0.19.11: + resolution: {integrity: sha512-ysyOGZuTp6SNKPE11INDUeFVVQFrhcNDVUgSQVDzqsqX38DjhPEPATpid04LCoUr2WXhQTEZ8ct/EgJCUDpyNw==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + requiresBuild: true + dev: false + optional: true + + /@esbuild/sunos-x64@0.19.11: + resolution: {integrity: sha512-Hf+Sad9nVwvtxy4DXCZQqLpgmRTQqyFyhT3bZ4F2XlJCjxGmRFF0Shwn9rzhOYRB61w9VMXUkxlBy56dk9JJiQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + requiresBuild: true + dev: false + optional: true + + /@esbuild/win32-arm64@0.19.11: + resolution: {integrity: sha512-0P58Sbi0LctOMOQbpEOvOL44Ne0sqbS0XWHMvvrg6NE5jQ1xguCSSw9jQeUk2lfrXYsKDdOe6K+oZiwKPilYPQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + requiresBuild: true + dev: false + optional: true + + /@esbuild/win32-ia32@0.19.11: + resolution: {integrity: sha512-6YOrWS+sDJDmshdBIQU+Uoyh7pQKrdykdefC1avn76ss5c+RN6gut3LZA4E2cH5xUEp5/cA0+YxRaVtRAb0xBg==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + requiresBuild: true + dev: false + optional: true + + /@esbuild/win32-x64@0.19.11: + resolution: {integrity: sha512-vfkhltrjCAb603XaFhqhAF4LGDi2M4OrCRrFusyQ+iTLQ/o60QQXxc9cZC/FFpihBI9N1Grn6SMKVJ4KP7Fuiw==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + requiresBuild: true + dev: false + optional: true + + /@eslint-community/eslint-utils@4.4.0(eslint@8.45.0): + resolution: {integrity: sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + dependencies: + eslint: 8.45.0 + eslint-visitor-keys: 3.4.3 + dev: false + + /@eslint-community/eslint-utils@4.4.0(eslint@8.56.0): + resolution: {integrity: sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + dependencies: + eslint: 8.56.0 + eslint-visitor-keys: 3.4.3 + dev: true + + /@eslint-community/regexpp@4.10.0: + resolution: {integrity: sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + + /@eslint/eslintrc@2.1.4: + resolution: {integrity: sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dependencies: + ajv: 6.12.6 + debug: 4.3.4 + espree: 9.6.1 + globals: 13.24.0 + ignore: 5.3.0 + import-fresh: 3.3.0 + js-yaml: 4.1.0 + minimatch: 3.1.2 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - supports-color + + /@eslint/js@8.44.0: + resolution: {integrity: sha512-Ag+9YM4ocKQx9AarydN0KY2j0ErMHNIocPDrVo8zAE44xLTjEtz81OdR68/cydGtk6m6jDb5Za3r2useMzYmSw==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dev: false + + /@eslint/js@8.56.0: + resolution: {integrity: sha512-gMsVel9D7f2HLkBma9VbtzZRehRogVRfbr++f06nL2vnCGCNlzOD+/MUov/F4p8myyAHspEhVobgjpX64q5m6A==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dev: true + + /@faker-js/faker@6.0.0: + resolution: {integrity: sha512-10zLCKhp3YEmBuko71ivcMoIZcCLXgQVck6aNswX+AWwaek/L8S3yz9i8m3tHigRkcF6F2vI+qtdtyySHK+bGA==} + engines: {node: '>=14.0.0', npm: '>=7.0.0'} + + /@graphql-tools/merge@8.4.2(graphql@16.8.1): + resolution: {integrity: sha512-XbrHAaj8yDuINph+sAfuq3QCZ/tKblrTLOpirK0+CAgNlZUCHs0Fa+xtMUURgwCVThLle1AF7svJCxFizygLsw==} + peerDependencies: + graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + dependencies: + '@graphql-tools/utils': 9.2.1(graphql@16.8.1) + graphql: 16.8.1 + tslib: 2.6.2 + dev: false + + /@graphql-tools/schema@9.0.19(graphql@16.8.1): + resolution: {integrity: sha512-oBRPoNBtCkk0zbUsyP4GaIzCt8C0aCI4ycIRUL67KK5pOHljKLBBtGT+Jr6hkzA74C8Gco8bpZPe7aWFjiaK2w==} + peerDependencies: + graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + dependencies: + '@graphql-tools/merge': 8.4.2(graphql@16.8.1) + '@graphql-tools/utils': 9.2.1(graphql@16.8.1) + graphql: 16.8.1 + tslib: 2.6.2 + value-or-promise: 1.0.12 + dev: false + + /@graphql-tools/utils@9.2.1(graphql@16.8.1): + resolution: {integrity: sha512-WUw506Ql6xzmOORlriNrD6Ugx+HjVgYxt9KCXD9mHAak+eaXSwuGGPyE60hy9xaDEoXKBsG7SkG69ybitaVl6A==} + peerDependencies: + graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + dependencies: + '@graphql-typed-document-node/core': 3.2.0(graphql@16.8.1) + graphql: 16.8.1 + tslib: 2.6.2 + dev: false + + /@graphql-typed-document-node/core@3.2.0(graphql@16.8.1): + resolution: {integrity: sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ==} + peerDependencies: + graphql: ^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + dependencies: + graphql: 16.8.1 + dev: false + + /@humanwhocodes/config-array@0.11.13: + resolution: {integrity: sha512-JSBDMiDKSzQVngfRjOdFXgFfklaXI4K9nLF49Auh21lmBWRLIK3+xTErTWD4KU54pb6coM6ESE7Awz/FNU3zgQ==} + engines: {node: '>=10.10.0'} + dependencies: + '@humanwhocodes/object-schema': 2.0.1 + debug: 4.3.4 + minimatch: 3.1.2 + transitivePeerDependencies: + - supports-color + dev: false + + /@humanwhocodes/config-array@0.11.14: + resolution: {integrity: sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==} + engines: {node: '>=10.10.0'} + dependencies: + '@humanwhocodes/object-schema': 2.0.2 + debug: 4.3.4 + minimatch: 3.1.2 + transitivePeerDependencies: + - supports-color + dev: true + + /@humanwhocodes/module-importer@1.0.1: + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} + engines: {node: '>=12.22'} + + /@humanwhocodes/object-schema@2.0.1: + resolution: {integrity: sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK+1GWi/yTvvLDHpoxLr0xxxeslWw==} + dev: false + + /@humanwhocodes/object-schema@2.0.2: + resolution: {integrity: sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw==} + dev: true + + /@inquirer/checkbox@1.5.0: + resolution: {integrity: sha512-3cKJkW1vIZAs4NaS0reFsnpAjP0azffYII4I2R7PTI7ZTMg5Y1at4vzXccOH3762b2c2L4drBhpJpf9uiaGNxA==} + engines: {node: '>=14.18.0'} + dependencies: + '@inquirer/core': 5.1.1 + '@inquirer/type': 1.1.5 + ansi-escapes: 4.3.2 + chalk: 4.1.2 + figures: 3.2.0 + dev: false + + /@inquirer/confirm@2.0.15: + resolution: {integrity: sha512-hj8Q/z7sQXsF0DSpLQZVDhWYGN6KLM/gNjjqGkpKwBzljbQofGjn0ueHADy4HUY+OqDHmXuwk/bY+tZyIuuB0w==} + engines: {node: '>=14.18.0'} + dependencies: + '@inquirer/core': 5.1.1 + '@inquirer/type': 1.1.5 + chalk: 4.1.2 + dev: false + + /@inquirer/core@2.3.1: + resolution: {integrity: sha512-faYAYnIfdEuns3jGKykaog5oUqFiEVbCx9nXGZfUhyEEpKcHt5bpJfZTb3eOBQKo8I/v4sJkZeBHmFlSZQuBCw==} + engines: {node: '>=14.18.0'} + dependencies: + '@inquirer/type': 1.1.5 + '@types/mute-stream': 0.0.1 + '@types/node': 20.10.8 + '@types/wrap-ansi': 3.0.0 + ansi-escapes: 4.3.2 + chalk: 4.1.2 + cli-spinners: 2.9.2 + cli-width: 4.1.0 + figures: 3.2.0 + mute-stream: 1.0.0 + run-async: 3.0.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 6.2.0 + dev: false + + /@inquirer/core@5.1.1: + resolution: {integrity: sha512-IuJyZQUg75+L5AmopgnzxYrgcU6PJKL0hoIs332G1Gv55CnmZrhG6BzNOeZ5sOsTi1YCGOopw4rYICv74ejMFg==} + engines: {node: '>=14.18.0'} + dependencies: + '@inquirer/type': 1.1.5 + '@types/mute-stream': 0.0.4 + '@types/node': 20.10.8 + '@types/wrap-ansi': 3.0.0 + ansi-escapes: 4.3.2 + chalk: 4.1.2 + cli-spinners: 2.9.2 + cli-width: 4.1.0 + figures: 3.2.0 + mute-stream: 1.0.0 + run-async: 3.0.0 + signal-exit: 4.1.0 + strip-ansi: 6.0.1 + wrap-ansi: 6.2.0 + dev: false + + /@inquirer/editor@1.2.13: + resolution: {integrity: sha512-gBxjqt0B9GLN0j6M/tkEcmcIvB2fo9Cw0f5NRqDTkYyB9AaCzj7qvgG0onQ3GVPbMyMbbP4tWYxrBOaOdKpzNA==} + engines: {node: '>=14.18.0'} + dependencies: + '@inquirer/core': 5.1.1 + '@inquirer/type': 1.1.5 + chalk: 4.1.2 + external-editor: 3.1.0 + dev: false + + /@inquirer/expand@1.1.14: + resolution: {integrity: sha512-yS6fJ8jZYAsxdxuw2c8XTFMTvMR1NxZAw3LxDaFnqh7BZ++wTQ6rSp/2gGJhMacdZ85osb+tHxjVgx7F+ilv5g==} + engines: {node: '>=14.18.0'} + dependencies: + '@inquirer/core': 5.1.1 + '@inquirer/type': 1.1.5 + chalk: 4.1.2 + figures: 3.2.0 + dev: false + + /@inquirer/input@1.2.14: + resolution: {integrity: sha512-tISLGpUKXixIQue7jypNEShrdzJoLvEvZOJ4QRsw5XTfrIYfoWFqAjMQLerGs9CzR86yAI89JR6snHmKwnNddw==} + engines: {node: '>=14.18.0'} + dependencies: + '@inquirer/core': 5.1.1 + '@inquirer/type': 1.1.5 + chalk: 4.1.2 + dev: false + + /@inquirer/password@1.1.14: + resolution: {integrity: sha512-vL2BFxfMo8EvuGuZYlryiyAB3XsgtbxOcFs4H9WI9szAS/VZCAwdVqs8rqEeaAf/GV/eZOghIOYxvD91IsRWSg==} + engines: {node: '>=14.18.0'} + dependencies: + '@inquirer/input': 1.2.14 + '@inquirer/type': 1.1.5 + ansi-escapes: 4.3.2 + chalk: 4.1.2 + dev: false + + /@inquirer/prompts@2.3.1: + resolution: {integrity: sha512-YQeBFzIE+6fcec5N/U2mSz+IcKEG4wtGDwF7MBLIDgITWzB3o723JpKJ1rxWqdCvTXkYE+gDXK/seSN6omo3DQ==} + engines: {node: '>=14.18.0'} + dependencies: + '@inquirer/checkbox': 1.5.0 + '@inquirer/confirm': 2.0.15 + '@inquirer/core': 2.3.1 + '@inquirer/editor': 1.2.13 + '@inquirer/expand': 1.1.14 + '@inquirer/input': 1.2.14 + '@inquirer/password': 1.1.14 + '@inquirer/rawlist': 1.2.14 + '@inquirer/select': 1.3.1 + dev: false + + /@inquirer/rawlist@1.2.14: + resolution: {integrity: sha512-xIYmDpYgfz2XGCKubSDLKEvadkIZAKbehHdWF082AyC2I4eHK44RUfXaoOAqnbqItZq4KHXS6jDJ78F2BmQvxg==} + engines: {node: '>=14.18.0'} + dependencies: + '@inquirer/core': 5.1.1 + '@inquirer/type': 1.1.5 + chalk: 4.1.2 + dev: false + + /@inquirer/select@1.3.1: + resolution: {integrity: sha512-EgOPHv7XOHEqiBwBJTyiMg9r57ySyW4oyYCumGp+pGyOaXQaLb2kTnccWI6NFd9HSi5kDJhF7YjA+3RfMQJ2JQ==} + engines: {node: '>=14.18.0'} + dependencies: + '@inquirer/core': 5.1.1 + '@inquirer/type': 1.1.5 + ansi-escapes: 4.3.2 + chalk: 4.1.2 + figures: 3.2.0 + dev: false + + /@inquirer/type@1.1.5: + resolution: {integrity: sha512-wmwHvHozpPo4IZkkNtbYenem/0wnfI6hvOcGKmPEa0DwuaH5XUQzFqy6OpEpjEegZMhYIk8HDYITI16BPLtrRA==} + engines: {node: '>=14.18.0'} + dev: false + + /@isaacs/cliui@8.0.2: + resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} + engines: {node: '>=12'} + dependencies: + string-width: 5.1.2 + string-width-cjs: /string-width@4.2.3 + strip-ansi: 7.1.0 + strip-ansi-cjs: /strip-ansi@6.0.1 + wrap-ansi: 8.1.0 + wrap-ansi-cjs: /wrap-ansi@7.0.0 + dev: false + + /@istanbuljs/load-nyc-config@1.1.0: + resolution: {integrity: sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==} + engines: {node: '>=8'} + dependencies: + camelcase: 5.3.1 + find-up: 4.1.0 + get-package-type: 0.1.0 + js-yaml: 3.14.1 + resolve-from: 5.0.0 + + /@istanbuljs/schema@0.1.3: + resolution: {integrity: sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==} + engines: {node: '>=8'} + + /@jest/console@28.1.3: + resolution: {integrity: sha512-QPAkP5EwKdK/bxIr6C1I4Vs0rm2nHiANzj/Z5X2JQkrZo6IqvC4ldZ9K95tF0HdidhA8Bo6egxSzUFPYKcEXLw==} + engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} + dependencies: + '@jest/types': 28.1.3 + '@types/node': 20.10.8 + chalk: 4.1.2 + jest-message-util: 28.1.3 + jest-util: 28.1.3 + slash: 3.0.0 + dev: true + + /@jest/console@29.7.0: + resolution: {integrity: sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/types': 29.6.3 + '@types/node': 20.10.8 + chalk: 4.1.2 + jest-message-util: 29.7.0 + jest-util: 29.7.0 + slash: 3.0.0 + + /@jest/core@28.1.3: + resolution: {integrity: sha512-CIKBrlaKOzA7YG19BEqCw3SLIsEwjZkeJzf5bdooVnW4bH5cktqe3JX+G2YV1aK5vP8N9na1IGWFzYaTp6k6NA==} + engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + dependencies: + '@jest/console': 28.1.3 + '@jest/reporters': 28.1.3 + '@jest/test-result': 28.1.3 + '@jest/transform': 28.1.3 + '@jest/types': 28.1.3 + '@types/node': 20.10.8 + ansi-escapes: 4.3.2 + chalk: 4.1.2 + ci-info: 3.9.0 + exit: 0.1.2 + graceful-fs: 4.2.11 + jest-changed-files: 28.1.3 + jest-config: 28.1.3(@types/node@20.10.8) + jest-haste-map: 28.1.3 + jest-message-util: 28.1.3 + jest-regex-util: 28.0.2 + jest-resolve: 28.1.3 + jest-resolve-dependencies: 28.1.3 + jest-runner: 28.1.3 + jest-runtime: 28.1.3 + jest-snapshot: 28.1.3 + jest-util: 28.1.3 + jest-validate: 28.1.3 + jest-watcher: 28.1.3 + micromatch: 4.0.5 + pretty-format: 28.1.3 + rimraf: 3.0.2 + slash: 3.0.0 + strip-ansi: 6.0.1 + transitivePeerDependencies: + - supports-color + - ts-node + dev: true + + /@jest/core@29.7.0(ts-node@10.9.2): + resolution: {integrity: sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + dependencies: + '@jest/console': 29.7.0 + '@jest/reporters': 29.7.0 + '@jest/test-result': 29.7.0 + '@jest/transform': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 20.10.8 + ansi-escapes: 4.3.2 + chalk: 4.1.2 + ci-info: 3.9.0 + exit: 0.1.2 + graceful-fs: 4.2.11 + jest-changed-files: 29.7.0 + jest-config: 29.7.0(@types/node@20.10.8)(ts-node@10.9.2) + jest-haste-map: 29.7.0 + jest-message-util: 29.7.0 + jest-regex-util: 29.6.3 + jest-resolve: 29.7.0 + jest-resolve-dependencies: 29.7.0 + jest-runner: 29.7.0 + jest-runtime: 29.7.0 + jest-snapshot: 29.7.0 + jest-util: 29.7.0 + jest-validate: 29.7.0 + jest-watcher: 29.7.0 + micromatch: 4.0.5 + pretty-format: 29.7.0 + slash: 3.0.0 + strip-ansi: 6.0.1 + transitivePeerDependencies: + - babel-plugin-macros + - supports-color + - ts-node + + /@jest/environment@28.1.3: + resolution: {integrity: sha512-1bf40cMFTEkKyEf585R9Iz1WayDjHoHqvts0XFYEqyKM3cFWDpeMoqKKTAF9LSYQModPUlh8FKptoM2YcMWAXA==} + engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} + dependencies: + '@jest/fake-timers': 28.1.3 + '@jest/types': 28.1.3 + '@types/node': 20.10.8 + jest-mock: 28.1.3 + dev: true + + /@jest/environment@29.7.0: + resolution: {integrity: sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/fake-timers': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 20.10.8 + jest-mock: 29.7.0 + + /@jest/expect-utils@28.1.3: + resolution: {integrity: sha512-wvbi9LUrHJLn3NlDW6wF2hvIMtd4JUl2QNVrjq+IBSHirgfrR3o9RnVtxzdEGO2n9JyIWwHnLfby5KzqBGg2YA==} + engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} + dependencies: + jest-get-type: 28.0.2 + dev: true + + /@jest/expect-utils@29.7.0: + resolution: {integrity: sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + jest-get-type: 29.6.3 + + /@jest/expect@28.1.3: + resolution: {integrity: sha512-lzc8CpUbSoE4dqT0U+g1qODQjBRHPpCPXissXD4mS9+sWQdmmpeJ9zSH1rS1HEkrsMN0fb7nKrJ9giAR1d3wBw==} + engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} + dependencies: + expect: 28.1.3 + jest-snapshot: 28.1.3 + transitivePeerDependencies: + - supports-color + dev: true + + /@jest/expect@29.7.0: + resolution: {integrity: sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + expect: 29.7.0 + jest-snapshot: 29.7.0 + transitivePeerDependencies: + - supports-color + + /@jest/fake-timers@28.1.3: + resolution: {integrity: sha512-D/wOkL2POHv52h+ok5Oj/1gOG9HSywdoPtFsRCUmlCILXNn5eIWmcnd3DIiWlJnpGvQtmajqBP95Ei0EimxfLw==} + engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} + dependencies: + '@jest/types': 28.1.3 + '@sinonjs/fake-timers': 9.1.2 + '@types/node': 20.10.8 + jest-message-util: 28.1.3 + jest-mock: 28.1.3 + jest-util: 28.1.3 + dev: true + + /@jest/fake-timers@29.7.0: + resolution: {integrity: sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/types': 29.6.3 + '@sinonjs/fake-timers': 10.3.0 + '@types/node': 20.10.8 + jest-message-util: 29.7.0 + jest-mock: 29.7.0 + jest-util: 29.7.0 + + /@jest/globals@28.1.3: + resolution: {integrity: sha512-XFU4P4phyryCXu1pbcqMO0GSQcYe1IsalYCDzRNyhetyeyxMcIxa11qPNDpVNLeretItNqEmYYQn1UYz/5x1NA==} + engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} + dependencies: + '@jest/environment': 28.1.3 + '@jest/expect': 28.1.3 + '@jest/types': 28.1.3 + transitivePeerDependencies: + - supports-color + dev: true + + /@jest/globals@29.7.0: + resolution: {integrity: sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/environment': 29.7.0 + '@jest/expect': 29.7.0 + '@jest/types': 29.6.3 + jest-mock: 29.7.0 + transitivePeerDependencies: + - supports-color + + /@jest/reporters@28.1.3: + resolution: {integrity: sha512-JuAy7wkxQZVNU/V6g9xKzCGC5LVXx9FDcABKsSXp5MiKPEE2144a/vXTEDoyzjUpZKfVwp08Wqg5A4WfTMAzjg==} + engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + dependencies: + '@bcoe/v8-coverage': 0.2.3 + '@jest/console': 28.1.3 + '@jest/test-result': 28.1.3 + '@jest/transform': 28.1.3 + '@jest/types': 28.1.3 + '@jridgewell/trace-mapping': 0.3.20 + '@types/node': 20.10.8 + chalk: 4.1.2 + collect-v8-coverage: 1.0.2 + exit: 0.1.2 + glob: 7.2.3 + graceful-fs: 4.2.11 + istanbul-lib-coverage: 3.2.2 + istanbul-lib-instrument: 5.2.1 + istanbul-lib-report: 3.0.1 + istanbul-lib-source-maps: 4.0.1 + istanbul-reports: 3.1.6 + jest-message-util: 28.1.3 + jest-util: 28.1.3 + jest-worker: 28.1.3 + slash: 3.0.0 + string-length: 4.0.2 + strip-ansi: 6.0.1 + terminal-link: 2.1.1 + v8-to-istanbul: 9.2.0 + transitivePeerDependencies: + - supports-color + dev: true + + /@jest/reporters@29.7.0: + resolution: {integrity: sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + dependencies: + '@bcoe/v8-coverage': 0.2.3 + '@jest/console': 29.7.0 + '@jest/test-result': 29.7.0 + '@jest/transform': 29.7.0 + '@jest/types': 29.6.3 + '@jridgewell/trace-mapping': 0.3.20 + '@types/node': 20.10.8 + chalk: 4.1.2 + collect-v8-coverage: 1.0.2 + exit: 0.1.2 + glob: 7.2.3 + graceful-fs: 4.2.11 + istanbul-lib-coverage: 3.2.2 + istanbul-lib-instrument: 6.0.1 + istanbul-lib-report: 3.0.1 + istanbul-lib-source-maps: 4.0.1 + istanbul-reports: 3.1.6 + jest-message-util: 29.7.0 + jest-util: 29.7.0 + jest-worker: 29.7.0 + slash: 3.0.0 + string-length: 4.0.2 + strip-ansi: 6.0.1 + v8-to-istanbul: 9.2.0 + transitivePeerDependencies: + - supports-color + + /@jest/schemas@28.1.3: + resolution: {integrity: sha512-/l/VWsdt/aBXgjshLWOFyFt3IVdYypu5y2Wn2rOO1un6nkqIn8SLXzgIMYXFyYsRWDyF5EthmKJMIdJvk08grg==} + engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} + dependencies: + '@sinclair/typebox': 0.24.51 + dev: true + + /@jest/schemas@29.6.3: + resolution: {integrity: sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@sinclair/typebox': 0.27.8 + + /@jest/source-map@28.1.2: + resolution: {integrity: sha512-cV8Lx3BeStJb8ipPHnqVw/IM2VCMWO3crWZzYodSIkxXnRcXJipCdx1JCK0K5MsJJouZQTH73mzf4vgxRaH9ww==} + engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} + dependencies: + '@jridgewell/trace-mapping': 0.3.20 + callsites: 3.1.0 + graceful-fs: 4.2.11 + dev: true + + /@jest/source-map@29.6.3: + resolution: {integrity: sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jridgewell/trace-mapping': 0.3.20 + callsites: 3.1.0 + graceful-fs: 4.2.11 + + /@jest/test-result@28.1.3: + resolution: {integrity: sha512-kZAkxnSE+FqE8YjW8gNuoVkkC9I7S1qmenl8sGcDOLropASP+BkcGKwhXoyqQuGOGeYY0y/ixjrd/iERpEXHNg==} + engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} + dependencies: + '@jest/console': 28.1.3 + '@jest/types': 28.1.3 + '@types/istanbul-lib-coverage': 2.0.6 + collect-v8-coverage: 1.0.2 + dev: true + + /@jest/test-result@29.7.0: + resolution: {integrity: sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/console': 29.7.0 + '@jest/types': 29.6.3 + '@types/istanbul-lib-coverage': 2.0.6 + collect-v8-coverage: 1.0.2 + + /@jest/test-sequencer@28.1.3: + resolution: {integrity: sha512-NIMPEqqa59MWnDi1kvXXpYbqsfQmSJsIbnd85mdVGkiDfQ9WQQTXOLsvISUfonmnBT+w85WEgneCigEEdHDFxw==} + engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} + dependencies: + '@jest/test-result': 28.1.3 + graceful-fs: 4.2.11 + jest-haste-map: 28.1.3 + slash: 3.0.0 + dev: true + + /@jest/test-sequencer@29.7.0: + resolution: {integrity: sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/test-result': 29.7.0 + graceful-fs: 4.2.11 + jest-haste-map: 29.7.0 + slash: 3.0.0 + + /@jest/transform@28.1.3: + resolution: {integrity: sha512-u5dT5di+oFI6hfcLOHGTAfmUxFRrjK+vnaP0kkVow9Md/M7V/MxqQMOz/VV25UZO8pzeA9PjfTpOu6BDuwSPQA==} + engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} + dependencies: + '@babel/core': 7.23.6 + '@jest/types': 28.1.3 + '@jridgewell/trace-mapping': 0.3.20 + babel-plugin-istanbul: 6.1.1 + chalk: 4.1.2 + convert-source-map: 1.9.0 + fast-json-stable-stringify: 2.1.0 + graceful-fs: 4.2.11 + jest-haste-map: 28.1.3 + jest-regex-util: 28.0.2 + jest-util: 28.1.3 + micromatch: 4.0.5 + pirates: 4.0.6 + slash: 3.0.0 + write-file-atomic: 4.0.2 + transitivePeerDependencies: + - supports-color + dev: true + + /@jest/transform@29.7.0: + resolution: {integrity: sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@babel/core': 7.23.7 + '@jest/types': 29.6.3 + '@jridgewell/trace-mapping': 0.3.20 + babel-plugin-istanbul: 6.1.1 + chalk: 4.1.2 + convert-source-map: 2.0.0 + fast-json-stable-stringify: 2.1.0 + graceful-fs: 4.2.11 + jest-haste-map: 29.7.0 + jest-regex-util: 29.6.3 + jest-util: 29.7.0 + micromatch: 4.0.5 + pirates: 4.0.6 + slash: 3.0.0 + write-file-atomic: 4.0.2 + transitivePeerDependencies: + - supports-color + + /@jest/types@28.1.3: + resolution: {integrity: sha512-RyjiyMUZrKz/c+zlMFO1pm70DcIlST8AeWTkoUdZevew44wcNZQHsEVOiCVtgVnlFFD82FPaXycys58cf2muVQ==} + engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} + dependencies: + '@jest/schemas': 28.1.3 + '@types/istanbul-lib-coverage': 2.0.6 + '@types/istanbul-reports': 3.0.4 + '@types/node': 20.10.8 + '@types/yargs': 17.0.32 + chalk: 4.1.2 + dev: true + + /@jest/types@29.6.3: + resolution: {integrity: sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/schemas': 29.6.3 + '@types/istanbul-lib-coverage': 2.0.6 + '@types/istanbul-reports': 3.0.4 + '@types/node': 20.10.8 + '@types/yargs': 17.0.32 + chalk: 4.1.2 + + /@josephg/resolvable@1.0.1: + resolution: {integrity: sha512-CtzORUwWTTOTqfVtHaKRJ0I1kNQd1bpn3sUh8I3nJDVY+5/M/Oe1DnEWzPQvqq/xPIIkzzzIP7mfCoAjFRvDhg==} + dev: false + + /@jridgewell/gen-mapping@0.3.3: + resolution: {integrity: sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==} + engines: {node: '>=6.0.0'} + dependencies: + '@jridgewell/set-array': 1.1.2 + '@jridgewell/sourcemap-codec': 1.4.15 + '@jridgewell/trace-mapping': 0.3.20 + + /@jridgewell/resolve-uri@3.1.1: + resolution: {integrity: sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==} + engines: {node: '>=6.0.0'} + + /@jridgewell/set-array@1.1.2: + resolution: {integrity: sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==} + engines: {node: '>=6.0.0'} + + /@jridgewell/sourcemap-codec@1.4.15: + resolution: {integrity: sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==} + + /@jridgewell/trace-mapping@0.3.20: + resolution: {integrity: sha512-R8LcPeWZol2zR8mmH3JeKQ6QRCFb7XgUhV9ZlGhHLGyg4wpPiPZNQOOWhFZhxKw8u//yTbNGI42Bx/3paXEQ+Q==} + dependencies: + '@jridgewell/resolve-uri': 3.1.1 + '@jridgewell/sourcemap-codec': 1.4.15 + + /@jridgewell/trace-mapping@0.3.9: + resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} + dependencies: + '@jridgewell/resolve-uri': 3.1.1 + '@jridgewell/sourcemap-codec': 1.4.15 + + /@jsii/check-node@1.93.0: + resolution: {integrity: sha512-NLn1Js6wEG2hYjH7gE5Q8s/hPlp3I+KhK/T8ykGdYVod7iODnk/0QVSZsk2iEyuw8NzvvgXUDBWreadUIWSz+g==} + engines: {node: '>= 14.17.0'} + dependencies: + chalk: 4.1.2 + semver: 7.5.4 + dev: false + + /@jsii/check-node@1.94.0: + resolution: {integrity: sha512-46W+V1oTFvF9ZpKpPYy//1WUmhZ8AD8O0ElmQtv9mundLHccZm+q7EmCYhozr7rlK5uSjU9/WHfbIx2DwynuJw==} + engines: {node: '>= 14.17.0'} + dependencies: + chalk: 4.1.2 + semver: 7.5.4 + dev: false + + /@jsii/spec@1.94.0: + resolution: {integrity: sha512-ur1aUMPsdZgflUIZC4feyJzrkGYzvtiIJxRowkSxr7Ip/sLCKvi61dvImWtJY9ZhEAl7Kiq7I/R32WVyxW0JrQ==} + engines: {node: '>= 14.17.0'} + dependencies: + ajv: 8.12.0 + dev: false + + /@microsoft/tsdoc-config@0.16.2: + resolution: {integrity: sha512-OGiIzzoBLgWWR0UdRJX98oYO+XKGf7tiK4Zk6tQ/E4IJqGCe7dvkTvgDZV5cFJUzLGDOjeAXrnZoA6QkVySuxw==} + dependencies: + '@microsoft/tsdoc': 0.14.2 + ajv: 6.12.6 + jju: 1.4.0 + resolve: 1.19.0 + dev: true + + /@microsoft/tsdoc@0.14.2: + resolution: {integrity: sha512-9b8mPpKrfeGRuhFH5iO1iwCLeIIsV6+H1sRfxbkoGXIyQE2BTsPd9zqSqQJ+pv5sJ/hT5M1zvOFL02MnEezFug==} + dev: true + + /@nicolo-ribaudo/eslint-scope-5-internals@5.1.1-v1: + resolution: {integrity: sha512-54/JRvkLIzzDWshCWfuhadfrfZVPiElY8Fcgmg1HroEly/EDSszzhBAsarCux+D/kOslTRquNzuyGSmUSTTHGg==} + dependencies: + eslint-scope: 5.1.1 + dev: true + + /@nodelib/fs.scandir@2.1.5: + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + + /@nodelib/fs.stat@2.0.5: + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + + /@nodelib/fs.walk@1.2.8: + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.16.0 + + /@octokit/auth-token@4.0.0: + resolution: {integrity: sha512-tY/msAuJo6ARbK6SPIxZrPBms3xPbfwBrulZe0Wtr/DIY9lje2HeV1uoebShn6mx7SjCHif6EjMvoREj+gZ+SA==} + engines: {node: '>= 18'} + dev: false + + /@octokit/core@5.0.2: + resolution: {integrity: sha512-cZUy1gUvd4vttMic7C0lwPed8IYXWYp8kHIMatyhY8t8n3Cpw2ILczkV5pGMPqef7v0bLo0pOHrEHarsau2Ydg==} + engines: {node: '>= 18'} + dependencies: + '@octokit/auth-token': 4.0.0 + '@octokit/graphql': 7.0.2 + '@octokit/request': 8.1.6 + '@octokit/request-error': 5.0.1 + '@octokit/types': 12.4.0 + before-after-hook: 2.2.3 + universal-user-agent: 6.0.1 + dev: false + + /@octokit/endpoint@9.0.4: + resolution: {integrity: sha512-DWPLtr1Kz3tv8L0UvXTDP1fNwM0S+z6EJpRcvH66orY6Eld4XBMCSYsaWp4xIm61jTWxK68BrR7ibO+vSDnZqw==} + engines: {node: '>= 18'} + dependencies: + '@octokit/types': 12.4.0 + universal-user-agent: 6.0.1 + dev: false + + /@octokit/graphql@7.0.2: + resolution: {integrity: sha512-OJ2iGMtj5Tg3s6RaXH22cJcxXRi7Y3EBqbHTBRq+PQAqfaS8f/236fUrWhfSn8P4jovyzqucxme7/vWSSZBX2Q==} + engines: {node: '>= 18'} + dependencies: + '@octokit/request': 8.1.6 + '@octokit/types': 12.4.0 + universal-user-agent: 6.0.1 + dev: false + + /@octokit/openapi-types@19.1.0: + resolution: {integrity: sha512-6G+ywGClliGQwRsjvqVYpklIfa7oRPA0vyhPQG/1Feh+B+wU0vGH1JiJ5T25d3g1JZYBHzR2qefLi9x8Gt+cpw==} + dev: false + + /@octokit/plugin-paginate-rest@9.1.5(@octokit/core@5.0.2): + resolution: {integrity: sha512-WKTQXxK+bu49qzwv4qKbMMRXej1DU2gq017euWyKVudA6MldaSSQuxtz+vGbhxV4CjxpUxjZu6rM2wfc1FiWVg==} + engines: {node: '>= 18'} + peerDependencies: + '@octokit/core': '>=5' + dependencies: + '@octokit/core': 5.0.2 + '@octokit/types': 12.4.0 + dev: false + + /@octokit/plugin-retry@6.0.1(@octokit/core@5.0.2): + resolution: {integrity: sha512-SKs+Tz9oj0g4p28qkZwl/topGcb0k0qPNX/i7vBKmDsjoeqnVfFUquqrE/O9oJY7+oLzdCtkiWSXLpLjvl6uog==} + engines: {node: '>= 18'} + peerDependencies: + '@octokit/core': '>=5' + dependencies: + '@octokit/core': 5.0.2 + '@octokit/request-error': 5.0.1 + '@octokit/types': 12.4.0 + bottleneck: 2.19.5 + dev: false + + /@octokit/plugin-throttling@8.1.3(@octokit/core@5.0.2): + resolution: {integrity: sha512-pfyqaqpc0EXh5Cn4HX9lWYsZ4gGbjnSmUILeu4u2gnuM50K/wIk9s1Pxt3lVeVwekmITgN/nJdoh43Ka+vye8A==} + engines: {node: '>= 18'} + peerDependencies: + '@octokit/core': ^5.0.0 + dependencies: + '@octokit/core': 5.0.2 + '@octokit/types': 12.4.0 + bottleneck: 2.19.5 + dev: false + + /@octokit/request-error@5.0.1: + resolution: {integrity: sha512-X7pnyTMV7MgtGmiXBwmO6M5kIPrntOXdyKZLigNfQWSEQzVxR4a4vo49vJjTWX70mPndj8KhfT4Dx+2Ng3vnBQ==} + engines: {node: '>= 18'} + dependencies: + '@octokit/types': 12.4.0 + deprecation: 2.3.1 + once: 1.4.0 + dev: false + + /@octokit/request@8.1.6: + resolution: {integrity: sha512-YhPaGml3ncZC1NfXpP3WZ7iliL1ap6tLkAp6MvbK2fTTPytzVUyUesBBogcdMm86uRYO5rHaM1xIWxigWZ17MQ==} + engines: {node: '>= 18'} + dependencies: + '@octokit/endpoint': 9.0.4 + '@octokit/request-error': 5.0.1 + '@octokit/types': 12.4.0 + universal-user-agent: 6.0.1 + dev: false + + /@octokit/types@12.4.0: + resolution: {integrity: sha512-FLWs/AvZllw/AGVs+nJ+ELCDZZJk+kY0zMen118xhL2zD0s1etIUHm1odgjP7epxYU1ln7SZxEUWYop5bhsdgQ==} + dependencies: + '@octokit/openapi-types': 19.1.0 + dev: false + + /@pkgjs/parseargs@0.11.0: + resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} + engines: {node: '>=14'} + requiresBuild: true + dev: false + optional: true + + /@pkgr/core@0.1.0: + resolution: {integrity: sha512-Zwq5OCzuwJC2jwqmpEQt7Ds1DTi6BWSwoGkbb1n9pO3hzb35BoJELx7c0T23iDkBGkh2e7tvOtjF3tr3OaQHDQ==} + engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} + + /@pnpm/config.env-replace@1.1.0: + resolution: {integrity: sha512-htyl8TWnKL7K/ESFa1oW2UB5lVDxuF5DpM7tBi6Hu2LNL3mWkIzNLG6N4zoCUP1lCKNxWy/3iu8mS8MvToGd6w==} + engines: {node: '>=12.22.0'} + dev: false + + /@pnpm/network.ca-file@1.0.2: + resolution: {integrity: sha512-YcPQ8a0jwYU9bTdJDpXjMi7Brhkr1mXsXrUJvjqM2mQDgkRiz8jFaQGOdaLxgjtUfQgZhKy/O3cG/YwmgKaxLA==} + engines: {node: '>=12.22.0'} + dependencies: + graceful-fs: 4.2.10 + dev: false + + /@pnpm/npm-conf@2.2.2: + resolution: {integrity: sha512-UA91GwWPhFExt3IizW6bOeY/pQ0BkuNwKjk9iQW9KqxluGCrg4VenZ0/L+2Y0+ZOtme72EVvg6v0zo3AMQRCeA==} + engines: {node: '>=12'} + dependencies: + '@pnpm/config.env-replace': 1.1.0 + '@pnpm/network.ca-file': 1.0.2 + config-chain: 1.1.13 + dev: false + + /@pocket-tools/apollo-utils@3.0.0: + resolution: {integrity: sha512-1VoVGVerrORxjLVFTEQJUm4wPQbXcCX1ZBt0Yukhu1QFU2uPCZryOP84reeDVICHJyL8dLRnt11a1Y2yIeqglw==} + dependencies: + '@apollo/server': 4.9.5(graphql@16.8.1) + '@apollo/subgraph': 2.6.2(graphql@16.8.1) + '@sentry/node': 7.91.0 + apollo-server-cache-redis: 3.3.1 + deep-equal-in-any-order: 1.1.20 + graphql: 16.8.1 + graphql-tag: 2.12.6(graphql@16.8.1) + md5: 2.3.0 + transitivePeerDependencies: + - encoding + - supports-color + dev: false + + /@pocket-tools/eslint-config@2.1.7(typescript@5.3.3): + resolution: {integrity: sha512-NXwPidPMa5rfYyAXdAQH1ooZ8IZIK3vSLpdGmvX5d4XEWdrf69vdpBcdHKn7GzBrAi2qltOpXc9kmrybTQ2HNg==} + engines: {node: '>=18'} + dependencies: + '@typescript-eslint/eslint-plugin': 6.1.0(@typescript-eslint/parser@6.1.0)(eslint@8.45.0)(typescript@5.3.3) + '@typescript-eslint/parser': 6.1.0(eslint@8.45.0)(typescript@5.3.3) + eslint: 8.45.0 + eslint-config-prettier: 8.8.0(eslint@8.45.0) + eslint-plugin-prettier: 5.0.0(eslint-config-prettier@8.8.0)(eslint@8.45.0)(prettier@3.0.0) + prettier: 3.0.0 + transitivePeerDependencies: + - '@types/eslint' + - supports-color + - typescript + dev: false + + /@pocket-tools/ts-logger@1.3.0(@babel/core@7.23.7)(@types/node@20.10.8)(typescript@5.3.3): + resolution: {integrity: sha512-SP+xuQQIuja3nFes681p2JgzmYJYmkoHmwDu3LrW6Td9tGffViQZiBl1uD47YTvfmMVtWHAaSRBZnaon6XkD1A==} + dependencies: + '@pocket-tools/eslint-config': 2.1.7(typescript@5.3.3) + '@pocket-tools/tsconfig': 2.0.2 + '@semantic-release/changelog': 6.0.3(semantic-release@21.1.2) + '@semantic-release/git': 10.0.1(semantic-release@21.1.2) + jest: 29.7.0(@types/node@20.10.8)(ts-node@10.9.2) + morgan: 1.10.0 + semantic-release: 21.1.2(typescript@5.3.3) + ts-jest: 29.1.1(@babel/core@7.23.7)(jest@29.7.0)(typescript@5.3.3) + ts-node: 10.9.2(@types/node@20.10.8)(typescript@5.3.3) + winston: 3.11.0 + transitivePeerDependencies: + - '@babel/core' + - '@jest/types' + - '@swc/core' + - '@swc/wasm' + - '@types/eslint' + - '@types/node' + - babel-jest + - babel-plugin-macros + - esbuild + - node-notifier + - supports-color + - typescript + dev: false + + /@pocket-tools/tsconfig@2.0.2: + resolution: {integrity: sha512-BpkJEYqpWf/8bWIBHTJGpuchYLwwTSAoajE/Y/vMVh5NASuauqvAwtVlyK9vbB1inI2d5/cjnPGeYww0C9C51g==} + engines: {node: '>=20'} + requiresBuild: true + dependencies: + package-up: 5.0.0 + typescript: 5.3.3 + dev: false + + /@protobufjs/aspromise@1.1.2: + resolution: {integrity: sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==} + dev: false + + /@protobufjs/base64@1.1.2: + resolution: {integrity: sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==} + dev: false + + /@protobufjs/codegen@2.0.4: + resolution: {integrity: sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==} + dev: false + + /@protobufjs/eventemitter@1.1.0: + resolution: {integrity: sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==} + dev: false + + /@protobufjs/fetch@1.1.0: + resolution: {integrity: sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==} + dependencies: + '@protobufjs/aspromise': 1.1.2 + '@protobufjs/inquire': 1.1.0 + dev: false + + /@protobufjs/float@1.0.2: + resolution: {integrity: sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==} + dev: false + + /@protobufjs/inquire@1.1.0: + resolution: {integrity: sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==} + dev: false + + /@protobufjs/path@1.1.2: + resolution: {integrity: sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==} + dev: false + + /@protobufjs/pool@1.1.0: + resolution: {integrity: sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==} + dev: false + + /@protobufjs/utf8@1.1.0: + resolution: {integrity: sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==} + dev: false + + /@rollup/rollup-android-arm-eabi@4.9.4: + resolution: {integrity: sha512-ub/SN3yWqIv5CWiAZPHVS1DloyZsJbtXmX4HxUTIpS0BHm9pW5iYBo2mIZi+hE3AeiTzHz33blwSnhdUo+9NpA==} + cpu: [arm] + os: [android] + requiresBuild: true + dev: false + optional: true + + /@rollup/rollup-android-arm64@4.9.4: + resolution: {integrity: sha512-ehcBrOR5XTl0W0t2WxfTyHCR/3Cq2jfb+I4W+Ch8Y9b5G+vbAecVv0Fx/J1QKktOrgUYsIKxWAKgIpvw56IFNA==} + cpu: [arm64] + os: [android] + requiresBuild: true + dev: false + optional: true + + /@rollup/rollup-darwin-arm64@4.9.4: + resolution: {integrity: sha512-1fzh1lWExwSTWy8vJPnNbNM02WZDS8AW3McEOb7wW+nPChLKf3WG2aG7fhaUmfX5FKw9zhsF5+MBwArGyNM7NA==} + cpu: [arm64] + os: [darwin] + requiresBuild: true + dev: false + optional: true + + /@rollup/rollup-darwin-x64@4.9.4: + resolution: {integrity: sha512-Gc6cukkF38RcYQ6uPdiXi70JB0f29CwcQ7+r4QpfNpQFVHXRd0DfWFidoGxjSx1DwOETM97JPz1RXL5ISSB0pA==} + cpu: [x64] + os: [darwin] + requiresBuild: true + dev: false + optional: true + + /@rollup/rollup-linux-arm-gnueabihf@4.9.4: + resolution: {integrity: sha512-g21RTeFzoTl8GxosHbnQZ0/JkuFIB13C3T7Y0HtKzOXmoHhewLbVTFBQZu+z5m9STH6FZ7L/oPgU4Nm5ErN2fw==} + cpu: [arm] + os: [linux] + requiresBuild: true + dev: false + optional: true + + /@rollup/rollup-linux-arm64-gnu@4.9.4: + resolution: {integrity: sha512-TVYVWD/SYwWzGGnbfTkrNpdE4HON46orgMNHCivlXmlsSGQOx/OHHYiQcMIOx38/GWgwr/po2LBn7wypkWw/Mg==} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: false + optional: true + + /@rollup/rollup-linux-arm64-musl@4.9.4: + resolution: {integrity: sha512-XcKvuendwizYYhFxpvQ3xVpzje2HHImzg33wL9zvxtj77HvPStbSGI9czrdbfrf8DGMcNNReH9pVZv8qejAQ5A==} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: false + optional: true + + /@rollup/rollup-linux-riscv64-gnu@4.9.4: + resolution: {integrity: sha512-LFHS/8Q+I9YA0yVETyjonMJ3UA+DczeBd/MqNEzsGSTdNvSJa1OJZcSH8GiXLvcizgp9AlHs2walqRcqzjOi3A==} + cpu: [riscv64] + os: [linux] + requiresBuild: true + dev: false + optional: true + + /@rollup/rollup-linux-x64-gnu@4.9.4: + resolution: {integrity: sha512-dIYgo+j1+yfy81i0YVU5KnQrIJZE8ERomx17ReU4GREjGtDW4X+nvkBak2xAUpyqLs4eleDSj3RrV72fQos7zw==} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: false + optional: true + + /@rollup/rollup-linux-x64-musl@4.9.4: + resolution: {integrity: sha512-RoaYxjdHQ5TPjaPrLsfKqR3pakMr3JGqZ+jZM0zP2IkDtsGa4CqYaWSfQmZVgFUCgLrTnzX+cnHS3nfl+kB6ZQ==} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: false + optional: true + + /@rollup/rollup-win32-arm64-msvc@4.9.4: + resolution: {integrity: sha512-T8Q3XHV+Jjf5e49B4EAaLKV74BbX7/qYBRQ8Wop/+TyyU0k+vSjiLVSHNWdVd1goMjZcbhDmYZUYW5RFqkBNHQ==} + cpu: [arm64] + os: [win32] + requiresBuild: true + dev: false + optional: true + + /@rollup/rollup-win32-ia32-msvc@4.9.4: + resolution: {integrity: sha512-z+JQ7JirDUHAsMecVydnBPWLwJjbppU+7LZjffGf+Jvrxq+dVjIE7By163Sc9DKc3ADSU50qPVw0KonBS+a+HQ==} + cpu: [ia32] + os: [win32] + requiresBuild: true + dev: false + optional: true + + /@rollup/rollup-win32-x64-msvc@4.9.4: + resolution: {integrity: sha512-LfdGXCV9rdEify1oxlN9eamvDSjv9md9ZVMAbNHA87xqIfFCxImxan9qZ8+Un54iK2nnqPlbnSi4R54ONtbWBw==} + cpu: [x64] + os: [win32] + requiresBuild: true + dev: false + optional: true + + /@rushstack/eslint-patch@1.6.1: + resolution: {integrity: sha512-UY+FGM/2jjMkzQLn8pxcHGMaVLh9aEitG3zY2CiY7XHdLiz3bZOwa6oDxNqEMv7zZkV+cj5DOdz0cQ1BP5Hjgw==} + dev: true + + /@semantic-release/changelog@6.0.3(semantic-release@21.1.2): + resolution: {integrity: sha512-dZuR5qByyfe3Y03TpmCvAxCyTnp7r5XwtHRf/8vD9EAn4ZWbavUX8adMtXYzE86EVh0gyLA7lm5yW4IV30XUag==} + engines: {node: '>=14.17'} + peerDependencies: + semantic-release: '>=18.0.0' + dependencies: + '@semantic-release/error': 3.0.0 + aggregate-error: 3.1.0 + fs-extra: 11.2.0 + lodash: 4.17.21 + semantic-release: 21.1.2(typescript@5.3.3) + dev: false + + /@semantic-release/commit-analyzer@10.0.4(semantic-release@21.1.2): + resolution: {integrity: sha512-pFGn99fn8w4/MHE0otb2A/l5kxgOuxaaauIh4u30ncoTJuqWj4hXTgEJ03REqjS+w1R2vPftSsO26WC61yOcpw==} + engines: {node: '>=18'} + peerDependencies: + semantic-release: '>=20.1.0' + dependencies: + conventional-changelog-angular: 6.0.0 + conventional-commits-filter: 3.0.0 + conventional-commits-parser: 5.0.0 + debug: 4.3.4 + import-from: 4.0.0 + lodash-es: 4.17.21 + micromatch: 4.0.5 + semantic-release: 21.1.2(typescript@5.3.3) + transitivePeerDependencies: + - supports-color + dev: false + + /@semantic-release/error@3.0.0: + resolution: {integrity: sha512-5hiM4Un+tpl4cKw3lV4UgzJj+SmfNIDCLLw0TepzQxz9ZGV5ixnqkzIVF+3tp0ZHgcMKE+VNGHJjEeyFG2dcSw==} + engines: {node: '>=14.17'} + dev: false + + /@semantic-release/error@4.0.0: + resolution: {integrity: sha512-mgdxrHTLOjOddRVYIYDo0fR3/v61GNN1YGkfbrjuIKg/uMgCd+Qzo3UAXJ+woLQQpos4pl5Esuw5A7AoNlzjUQ==} + engines: {node: '>=18'} + dev: false + + /@semantic-release/git@10.0.1(semantic-release@21.1.2): + resolution: {integrity: sha512-eWrx5KguUcU2wUPaO6sfvZI0wPafUKAMNC18aXY4EnNcrZL86dEmpNVnC9uMpGZkmZJ9EfCVJBQx4pV4EMGT1w==} + engines: {node: '>=14.17'} + peerDependencies: + semantic-release: '>=18.0.0' + dependencies: + '@semantic-release/error': 3.0.0 + aggregate-error: 3.1.0 + debug: 4.3.4 + dir-glob: 3.0.1 + execa: 5.1.1 + lodash: 4.17.21 + micromatch: 4.0.5 + p-reduce: 2.1.0 + semantic-release: 21.1.2(typescript@5.3.3) + transitivePeerDependencies: + - supports-color + dev: false + + /@semantic-release/github@9.2.6(semantic-release@21.1.2): + resolution: {integrity: sha512-shi+Lrf6exeNZF+sBhK+P011LSbhmIAoUEgEY6SsxF8irJ+J2stwI5jkyDQ+4gzYyDImzV6LCKdYB9FXnQRWKA==} + engines: {node: '>=18'} + peerDependencies: + semantic-release: '>=20.1.0' + dependencies: + '@octokit/core': 5.0.2 + '@octokit/plugin-paginate-rest': 9.1.5(@octokit/core@5.0.2) + '@octokit/plugin-retry': 6.0.1(@octokit/core@5.0.2) + '@octokit/plugin-throttling': 8.1.3(@octokit/core@5.0.2) + '@semantic-release/error': 4.0.0 + aggregate-error: 5.0.0 + debug: 4.3.4 + dir-glob: 3.0.1 + globby: 14.0.0 + http-proxy-agent: 7.0.0 + https-proxy-agent: 7.0.2 + issue-parser: 6.0.0 + lodash-es: 4.17.21 + mime: 4.0.1 + p-filter: 4.1.0 + semantic-release: 21.1.2(typescript@5.3.3) + url-join: 5.0.0 + transitivePeerDependencies: + - supports-color + dev: false + + /@semantic-release/npm@10.0.6(semantic-release@21.1.2): + resolution: {integrity: sha512-DyqHrGE8aUyapA277BB+4kV0C4iMHh3sHzUWdf0jTgp5NNJxVUz76W1f57FB64Ue03him3CBXxFqQD2xGabxow==} + engines: {node: '>=18'} + peerDependencies: + semantic-release: '>=20.1.0' + dependencies: + '@semantic-release/error': 4.0.0 + aggregate-error: 5.0.0 + execa: 8.0.1 + fs-extra: 11.2.0 + lodash-es: 4.17.21 + nerf-dart: 1.0.0 + normalize-url: 8.0.0 + npm: 9.9.2 + rc: 1.2.8 + read-pkg: 8.1.0 + registry-auth-token: 5.0.2 + semantic-release: 21.1.2(typescript@5.3.3) + semver: 7.5.4 + tempy: 3.1.0 + dev: false + + /@semantic-release/release-notes-generator@11.0.7(semantic-release@21.1.2): + resolution: {integrity: sha512-T09QB9ImmNx7Q6hY6YnnEbw/rEJ6a+22LBxfZq+pSAXg/OL/k0siwEm5cK4k1f9dE2Z2mPIjJKKohzUm0jbxcQ==} + engines: {node: '>=18'} + peerDependencies: + semantic-release: '>=20.1.0' + dependencies: + conventional-changelog-angular: 6.0.0 + conventional-changelog-writer: 6.0.1 + conventional-commits-filter: 4.0.0 + conventional-commits-parser: 5.0.0 + debug: 4.3.4 + get-stream: 7.0.1 + import-from: 4.0.0 + into-stream: 7.0.0 + lodash-es: 4.17.21 + read-pkg-up: 10.1.0 + semantic-release: 21.1.2(typescript@5.3.3) + transitivePeerDependencies: + - supports-color + dev: false + + /@sentry-internal/tracing@7.91.0: + resolution: {integrity: sha512-JH5y6gs6BS0its7WF2DhySu7nkhPDfZcdpAXldxzIlJpqFkuwQKLU5nkYJpiIyZz1NHYYtW5aum2bV2oCOdDRA==} + engines: {node: '>=8'} + dependencies: + '@sentry/core': 7.91.0 + '@sentry/types': 7.91.0 + '@sentry/utils': 7.91.0 + dev: false + + /@sentry-internal/tracing@7.92.0: + resolution: {integrity: sha512-ur55vPcUUUWFUX4eVLNP71ohswK7ZZpleNZw9Y1GfLqyI+0ILQUwjtzqItJrdClvVsdRZJMRmDV40Hp9Lbb9mA==} + engines: {node: '>=8'} + dependencies: + '@sentry/core': 7.92.0 + '@sentry/types': 7.92.0 + '@sentry/utils': 7.92.0 + dev: false + + /@sentry/core@6.19.7: + resolution: {integrity: sha512-tOfZ/umqB2AcHPGbIrsFLcvApdTm9ggpi/kQZFkej7kMphjT+SGBiQfYtjyg9jcRW+ilAR4JXC9BGKsdEQ+8Vw==} + engines: {node: '>=6'} + dependencies: + '@sentry/hub': 6.19.7 + '@sentry/minimal': 6.19.7 + '@sentry/types': 6.19.7 + '@sentry/utils': 6.19.7 + tslib: 1.14.1 + dev: false + + /@sentry/core@7.91.0: + resolution: {integrity: sha512-tu+gYq4JrTdrR+YSh5IVHF0fJi/Pi9y0HZ5H9HnYy+UMcXIotxf6hIEaC6ZKGeLWkGXffz2gKpQLe/g6vy/lPA==} + engines: {node: '>=8'} + dependencies: + '@sentry/types': 7.91.0 + '@sentry/utils': 7.91.0 + dev: false + + /@sentry/core@7.92.0: + resolution: {integrity: sha512-1Tly7YB2I1byI5xb0Cwrxs56Rhww+6mQ7m9P7rTmdC3/ijOzbEoohtYIUPwcooCEarpbEJe/tAayRx6BrH2UbQ==} + engines: {node: '>=8'} + dependencies: + '@sentry/types': 7.92.0 + '@sentry/utils': 7.92.0 + dev: false + + /@sentry/hub@6.19.7: + resolution: {integrity: sha512-y3OtbYFAqKHCWezF0EGGr5lcyI2KbaXW2Ik7Xp8Mu9TxbSTuwTe4rTntwg8ngPjUQU3SUHzgjqVB8qjiGqFXCA==} + engines: {node: '>=6'} + dependencies: + '@sentry/types': 6.19.7 + '@sentry/utils': 6.19.7 + tslib: 1.14.1 + dev: false + + /@sentry/minimal@6.19.7: + resolution: {integrity: sha512-wcYmSJOdvk6VAPx8IcmZgN08XTXRwRtB1aOLZm+MVHjIZIhHoBGZJYTVQS/BWjldsamj2cX3YGbGXNunaCfYJQ==} + engines: {node: '>=6'} + dependencies: + '@sentry/hub': 6.19.7 + '@sentry/types': 6.19.7 + tslib: 1.14.1 + dev: false + + /@sentry/node@6.19.7: + resolution: {integrity: sha512-gtmRC4dAXKODMpHXKfrkfvyBL3cI8y64vEi3fDD046uqYcrWdgoQsffuBbxMAizc6Ez1ia+f0Flue6p15Qaltg==} + engines: {node: '>=6'} + dependencies: + '@sentry/core': 6.19.7 + '@sentry/hub': 6.19.7 + '@sentry/types': 6.19.7 + '@sentry/utils': 6.19.7 + cookie: 0.4.2 + https-proxy-agent: 5.0.1 + lru_map: 0.3.3 + tslib: 1.14.1 + transitivePeerDependencies: + - supports-color + dev: false + + /@sentry/node@7.91.0: + resolution: {integrity: sha512-hTIfSQxD7L+AKIqyjoq8CWBRkEQrrMZmA3GSZgPI5JFWBHgO0HBo5TH/8TU81oEJh6kqqHAl2ObMhmcnaFqlzg==} + engines: {node: '>=8'} + dependencies: + '@sentry-internal/tracing': 7.91.0 + '@sentry/core': 7.91.0 + '@sentry/types': 7.91.0 + '@sentry/utils': 7.91.0 + https-proxy-agent: 5.0.1 + transitivePeerDependencies: + - supports-color + dev: false + + /@sentry/node@7.92.0: + resolution: {integrity: sha512-LZeQL1r6kikEoOzA9K61OmMl32/lK/6PzmFNDH6z7UYwQopCZgVA6IP+CZuln8K2ys5c9hCyF7ICQMysXfpNJA==} + engines: {node: '>=8'} + dependencies: + '@sentry-internal/tracing': 7.92.0 + '@sentry/core': 7.92.0 + '@sentry/types': 7.92.0 + '@sentry/utils': 7.92.0 + https-proxy-agent: 5.0.1 + transitivePeerDependencies: + - supports-color + dev: false + + /@sentry/serverless@6.19.7: + resolution: {integrity: sha512-uGOfoh3DH+fvUu7LhMLlnd4sAPdBfzFnfQNHnfw8jF/6tC53wdEDH/bBS9d+g8OC37ixJ+jrZIV1lHF5ann37w==} + engines: {node: '>=10'} + dependencies: + '@sentry/minimal': 6.19.7 + '@sentry/node': 6.19.7 + '@sentry/tracing': 6.19.7 + '@sentry/types': 6.19.7 + '@sentry/utils': 6.19.7 + '@types/aws-lambda': 8.10.119 + '@types/express': 4.17.21 + tslib: 1.14.1 + transitivePeerDependencies: + - supports-color + dev: false + + /@sentry/tracing@6.19.7: + resolution: {integrity: sha512-ol4TupNnv9Zd+bZei7B6Ygnr9N3Gp1PUrNI761QSlHtPC25xXC5ssSD3GMhBgyQrcvpuRcCFHVNNM97tN5cZiA==} + engines: {node: '>=6'} + dependencies: + '@sentry/hub': 6.19.7 + '@sentry/minimal': 6.19.7 + '@sentry/types': 6.19.7 + '@sentry/utils': 6.19.7 + tslib: 1.14.1 + dev: false + + /@sentry/types@6.19.7: + resolution: {integrity: sha512-jH84pDYE+hHIbVnab3Hr+ZXr1v8QABfhx39KknxqKWr2l0oEItzepV0URvbEhB446lk/S/59230dlUUIBGsXbg==} + engines: {node: '>=6'} + dev: false + + /@sentry/types@7.91.0: + resolution: {integrity: sha512-bcQnb7J3P3equbCUc+sPuHog2Y47yGD2sCkzmnZBjvBT0Z1B4f36fI/5WjyZhTjLSiOdg3F2otwvikbMjmBDew==} + engines: {node: '>=8'} + dev: false + + /@sentry/types@7.92.0: + resolution: {integrity: sha512-APmSOuZuoRGpbPpPeYIbMSplPjiWNLZRQa73QiXuTflW4Tu/ItDlU8hOa2+A6JKVkJCuD2EN6yUrxDGSMyNXeg==} + engines: {node: '>=8'} + dev: false + + /@sentry/utils@6.19.7: + resolution: {integrity: sha512-z95ECmE3i9pbWoXQrD/7PgkBAzJYR+iXtPuTkpBjDKs86O3mT+PXOT3BAn79w2wkn7/i3vOGD2xVr1uiMl26dA==} + engines: {node: '>=6'} + dependencies: + '@sentry/types': 6.19.7 + tslib: 1.14.1 + dev: false + + /@sentry/utils@7.91.0: + resolution: {integrity: sha512-fvxjrEbk6T6Otu++Ax9ntlQ0sGRiwSC179w68aC3u26Wr30FAIRKqHTCCdc2jyWk7Gd9uWRT/cq+g8NG/8BfSg==} + engines: {node: '>=8'} + dependencies: + '@sentry/types': 7.91.0 + dev: false + + /@sentry/utils@7.92.0: + resolution: {integrity: sha512-3nEfrQ1z28b/2zgFGANPh5yMVtgwXmrasZxTvKbrAj+KWJpjrJHrIR84r9W277J44NMeZ5RhRW2uoDmuBslPnA==} + engines: {node: '>=8'} + dependencies: + '@sentry/types': 7.92.0 + dev: false + + /@sinclair/typebox@0.24.51: + resolution: {integrity: sha512-1P1OROm/rdubP5aFDSZQILU0vrLCJ4fvHt6EoqHEM+2D/G5MK3bIaymUKLit8Js9gbns5UyJnkP/TZROLw4tUA==} + dev: true + + /@sinclair/typebox@0.27.8: + resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==} + + /@sindresorhus/is@4.6.0: + resolution: {integrity: sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==} + engines: {node: '>=10'} + dev: false + + /@sindresorhus/merge-streams@1.0.0: + resolution: {integrity: sha512-rUV5WyJrJLoloD4NDN1V1+LDMDWOa4OTsT4yYJwQNpTU6FWxkxHpL7eu4w+DmiH8x/EAM1otkPE1+LaspIbplw==} + engines: {node: '>=18'} + dev: false + + /@sinonjs/commons@1.8.6: + resolution: {integrity: sha512-Ky+XkAkqPZSm3NLBeUng77EBQl3cmeJhITaGHdYH8kjVB+aun3S4XBRti2zt17mtt0mIUDiNxYeoJm6drVvBJQ==} + dependencies: + type-detect: 4.0.8 + dev: true + + /@sinonjs/commons@2.0.0: + resolution: {integrity: sha512-uLa0j859mMrg2slwQYdO/AkrOfmH+X6LTVmNTS9CqexuE2IvVORIkSpJLqePAbEnKJ77aMmCwr1NUZ57120Xcg==} + dependencies: + type-detect: 4.0.8 + dev: true + + /@sinonjs/commons@3.0.0: + resolution: {integrity: sha512-jXBtWAF4vmdNmZgD5FoKsVLv3rPgDnLgPbU84LIJ3otV44vJlDRokVng5v8NFJdCf/da9legHcKaRuZs4L7faA==} + dependencies: + type-detect: 4.0.8 + + /@sinonjs/fake-timers@10.3.0: + resolution: {integrity: sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==} + dependencies: + '@sinonjs/commons': 3.0.0 + + /@sinonjs/fake-timers@11.2.2: + resolution: {integrity: sha512-G2piCSxQ7oWOxwGSAyFHfPIsyeJGXYtc6mFbnFA+kRXkiEnTl8c/8jul2S329iFBnDI9HGoeWWAZvuvOkZccgw==} + dependencies: + '@sinonjs/commons': 3.0.0 + dev: true + + /@sinonjs/fake-timers@9.1.2: + resolution: {integrity: sha512-BPS4ynJW/o92PUR4wgriz2Ud5gpST5vz6GQfMixEDK0Z8ZCUv2M7SkBLykH56T++Xs+8ln9zTGbOvNGIe02/jw==} + dependencies: + '@sinonjs/commons': 1.8.6 + dev: true + + /@sinonjs/samsam@8.0.0: + resolution: {integrity: sha512-Bp8KUVlLp8ibJZrnvq2foVhP0IVX2CIprMJPK0vqGqgrDa0OHVKeZyBykqskkrdxV6yKBPmGasO8LVjAKR3Gew==} + dependencies: + '@sinonjs/commons': 2.0.0 + lodash.get: 4.4.2 + type-detect: 4.0.8 + dev: true + + /@sinonjs/text-encoding@0.7.2: + resolution: {integrity: sha512-sXXKG+uL9IrKqViTtao2Ws6dy0znu9sOaP1di/jKGW1M6VssO8vlpXCQcpZ+jisQ1tTFAC5Jo/EOzFbggBagFQ==} + dev: true + + /@smithy/abort-controller@2.0.15: + resolution: {integrity: sha512-JkS36PIS3/UCbq/MaozzV7jECeL+BTt4R75bwY8i+4RASys4xOyUS1HsRyUNSqUXFP4QyCz5aNnh3ltuaxv+pw==} + engines: {node: '>=14.0.0'} + dependencies: + '@smithy/types': 2.7.0 + tslib: 2.6.2 + dev: false + + /@smithy/config-resolver@2.0.21: + resolution: {integrity: sha512-rlLIGT+BeqjnA6C2FWumPRJS1UW07iU5ZxDHtFuyam4W65gIaOFMjkB90ofKCIh+0mLVQrQFrl/VLtQT/6FWTA==} + engines: {node: '>=14.0.0'} + dependencies: + '@smithy/node-config-provider': 2.1.8 + '@smithy/types': 2.7.0 + '@smithy/util-config-provider': 2.0.0 + '@smithy/util-middleware': 2.0.8 + tslib: 2.6.2 + dev: false + + /@smithy/credential-provider-imds@2.1.4: + resolution: {integrity: sha512-cwPJN1fa1YOQzhBlTXRavABEYRRchci1X79QRwzaNLySnIMJfztyv1Zkst0iZPLMnpn8+CnHu3wOHS11J5Dr3A==} + engines: {node: '>=14.0.0'} + dependencies: + '@smithy/node-config-provider': 2.1.8 + '@smithy/property-provider': 2.0.16 + '@smithy/types': 2.7.0 + '@smithy/url-parser': 2.0.15 + tslib: 2.6.2 + dev: false + + /@smithy/eventstream-codec@2.0.15: + resolution: {integrity: sha512-crjvz3j1gGPwA0us6cwS7+5gAn35CTmqu/oIxVbYJo2Qm/sGAye6zGJnMDk3BKhWZw5kcU1G4MxciTkuBpOZPg==} + dependencies: + '@aws-crypto/crc32': 3.0.0 + '@smithy/types': 2.7.0 + '@smithy/util-hex-encoding': 2.0.0 + tslib: 2.6.2 + dev: false + + /@smithy/eventstream-serde-browser@2.0.15: + resolution: {integrity: sha512-WiFG5N9j3jmS5P0z5Xev6dO0c3lf7EJYC2Ncb0xDnWFvShwXNn741AF71ABr5EcZw8F4rQma0362MMjAwJeZog==} + engines: {node: '>=14.0.0'} + dependencies: + '@smithy/eventstream-serde-universal': 2.0.15 + '@smithy/types': 2.7.0 + tslib: 2.6.2 + dev: false + + /@smithy/eventstream-serde-config-resolver@2.0.15: + resolution: {integrity: sha512-o65d2LRjgCbWYH+VVNlWXtmsI231SO99ZTOL4UuIPa6WTjbSHWtlXvUcJG9libhEKWmEV9DIUiH2IqyPWi7ubA==} + engines: {node: '>=14.0.0'} + dependencies: + '@smithy/types': 2.7.0 + tslib: 2.6.2 + dev: false + + /@smithy/eventstream-serde-node@2.0.15: + resolution: {integrity: sha512-9OOXiIhHq1VeOG6xdHkn2ZayfMYM3vzdUTV3zhcCnt+tMqA3BJK3XXTJFRR2BV28rtRM778DzqbBTf+hqwQPTg==} + engines: {node: '>=14.0.0'} + dependencies: + '@smithy/eventstream-serde-universal': 2.0.15 + '@smithy/types': 2.7.0 + tslib: 2.6.2 + dev: false + + /@smithy/eventstream-serde-universal@2.0.15: + resolution: {integrity: sha512-dP8AQp/pXlWBjvL0TaPBJC3rM0GoYv7O0Uim8d/7UKZ2Wo13bFI3/BhQfY/1DeiP1m23iCHFNFtOQxfQNBB8rQ==} + engines: {node: '>=14.0.0'} + dependencies: + '@smithy/eventstream-codec': 2.0.15 + '@smithy/types': 2.7.0 + tslib: 2.6.2 + dev: false + + /@smithy/fetch-http-handler@2.3.1: + resolution: {integrity: sha512-6MNk16fqb8EwcYY8O8WxB3ArFkLZ2XppsSNo1h7SQcFdDDwIumiJeO6wRzm7iB68xvsOQzsdQKbdtTieS3hfSQ==} + dependencies: + '@smithy/protocol-http': 3.0.11 + '@smithy/querystring-builder': 2.0.15 + '@smithy/types': 2.7.0 + '@smithy/util-base64': 2.0.1 + tslib: 2.6.2 + dev: false + + /@smithy/hash-node@2.0.17: + resolution: {integrity: sha512-Il6WuBcI1nD+e2DM7tTADMf01wEPGK8PAhz4D+YmDUVaoBqlA+CaH2uDJhiySifmuKBZj748IfygXty81znKhw==} + engines: {node: '>=14.0.0'} + dependencies: + '@smithy/types': 2.7.0 + '@smithy/util-buffer-from': 2.0.0 + '@smithy/util-utf8': 2.0.2 + tslib: 2.6.2 + dev: false + + /@smithy/invalid-dependency@2.0.15: + resolution: {integrity: sha512-dlEKBFFwVfzA5QroHlBS94NpgYjXhwN/bFfun+7w3rgxNvVy79SK0w05iGc7UAeC5t+D7gBxrzdnD6hreZnDVQ==} + dependencies: + '@smithy/types': 2.7.0 + tslib: 2.6.2 + dev: false + + /@smithy/is-array-buffer@2.0.0: + resolution: {integrity: sha512-z3PjFjMyZNI98JFRJi/U0nGoLWMSJlDjAW4QUX2WNZLas5C0CmVV6LJ01JI0k90l7FvpmixjWxPFmENSClQ7ug==} + engines: {node: '>=14.0.0'} + dependencies: + tslib: 2.6.2 + dev: false + + /@smithy/middleware-content-length@2.0.17: + resolution: {integrity: sha512-OyadvMcKC7lFXTNBa8/foEv7jOaqshQZkjWS9coEXPRZnNnihU/Ls+8ZuJwGNCOrN2WxXZFmDWhegbnM4vak8w==} + engines: {node: '>=14.0.0'} + dependencies: + '@smithy/protocol-http': 3.0.11 + '@smithy/types': 2.7.0 + tslib: 2.6.2 + dev: false + + /@smithy/middleware-endpoint@2.2.3: + resolution: {integrity: sha512-nYfxuq0S/xoAjdLbyn1ixeVB6cyH9wYCMtbbOCpcCRYR5u2mMtqUtVjjPAZ/DIdlK3qe0tpB0Q76szFGNuz+kQ==} + engines: {node: '>=14.0.0'} + dependencies: + '@smithy/middleware-serde': 2.0.15 + '@smithy/node-config-provider': 2.1.8 + '@smithy/shared-ini-file-loader': 2.2.7 + '@smithy/types': 2.7.0 + '@smithy/url-parser': 2.0.15 + '@smithy/util-middleware': 2.0.8 + tslib: 2.6.2 + dev: false + + /@smithy/middleware-retry@2.0.25: + resolution: {integrity: sha512-FXhafCPvx/9L9OgHJ3cdo/pD1f7ngC7DKsjDV2J7k6LO/Yl69POoBLk4sI1OZPUGc4dfxriENlTma9Nj1hI+IQ==} + engines: {node: '>=14.0.0'} + dependencies: + '@smithy/node-config-provider': 2.1.8 + '@smithy/protocol-http': 3.0.11 + '@smithy/service-error-classification': 2.0.8 + '@smithy/smithy-client': 2.2.0 + '@smithy/types': 2.7.0 + '@smithy/util-middleware': 2.0.8 + '@smithy/util-retry': 2.0.8 + tslib: 2.6.2 + uuid: 8.3.2 + dev: false + + /@smithy/middleware-serde@2.0.15: + resolution: {integrity: sha512-FOZRFk/zN4AT4wzGuBY+39XWe+ZnCFd0gZtyw3f9Okn2CJPixl9GyWe98TIaljeZdqWkgrzGyPre20AcW2UMHQ==} + engines: {node: '>=14.0.0'} + dependencies: + '@smithy/types': 2.7.0 + tslib: 2.6.2 + dev: false + + /@smithy/middleware-stack@2.0.9: + resolution: {integrity: sha512-bCB5dUtGQ5wh7QNL2ELxmDc6g7ih7jWU3Kx6MYH1h4mZbv9xL3WyhKHojRltThCB1arLPyTUFDi+x6fB/oabtA==} + engines: {node: '>=14.0.0'} + dependencies: + '@smithy/types': 2.7.0 + tslib: 2.6.2 + dev: false + + /@smithy/node-config-provider@2.1.8: + resolution: {integrity: sha512-+w26OKakaBUGp+UG+dxYZtFb5fs3tgHg3/QrRrmUZj+rl3cIuw840vFUXX35cVPTUCQIiTqmz7CpVF7+hdINdQ==} + engines: {node: '>=14.0.0'} + dependencies: + '@smithy/property-provider': 2.0.16 + '@smithy/shared-ini-file-loader': 2.2.7 + '@smithy/types': 2.7.0 + tslib: 2.6.2 + dev: false + + /@smithy/node-http-handler@2.2.1: + resolution: {integrity: sha512-8iAKQrC8+VFHPAT8pg4/j6hlsTQh+NKOWlctJBrYtQa4ExcxX7aSg3vdQ2XLoYwJotFUurg/NLqFCmZaPRrogw==} + engines: {node: '>=14.0.0'} + dependencies: + '@smithy/abort-controller': 2.0.15 + '@smithy/protocol-http': 3.0.11 + '@smithy/querystring-builder': 2.0.15 + '@smithy/types': 2.7.0 + tslib: 2.6.2 + dev: false + + /@smithy/property-provider@2.0.16: + resolution: {integrity: sha512-28Ky0LlOqtEjwg5CdHmwwaDRHcTWfPRzkT6HrhwOSRS2RryAvuDfJrZpM+BMcrdeCyEg1mbcgIMoqTla+rdL8Q==} + engines: {node: '>=14.0.0'} + dependencies: + '@smithy/types': 2.7.0 + tslib: 2.6.2 + dev: false + + /@smithy/protocol-http@1.2.0: + resolution: {integrity: sha512-GfGfruksi3nXdFok5RhgtOnWe5f6BndzYfmEXISD+5gAGdayFGpjWu5pIqIweTudMtse20bGbc+7MFZXT1Tb8Q==} + engines: {node: '>=14.0.0'} + dependencies: + '@smithy/types': 1.2.0 + tslib: 2.6.2 + dev: false + + /@smithy/protocol-http@2.0.5: + resolution: {integrity: sha512-d2hhHj34mA2V86doiDfrsy2fNTnUOowGaf9hKb0hIPHqvcnShU4/OSc4Uf1FwHkAdYF3cFXTrj5VGUYbEuvMdw==} + engines: {node: '>=14.0.0'} + dependencies: + '@smithy/types': 2.7.0 + tslib: 2.6.2 + dev: false + + /@smithy/protocol-http@3.0.11: + resolution: {integrity: sha512-3ziB8fHuXIRamV/akp/sqiWmNPR6X+9SB8Xxnozzj+Nq7hSpyKdFHd1FLpBkgfGFUTzzcBJQlDZPSyxzmdcx5A==} + engines: {node: '>=14.0.0'} + dependencies: + '@smithy/types': 2.7.0 + tslib: 2.6.2 + dev: false + + /@smithy/querystring-builder@2.0.15: + resolution: {integrity: sha512-e1q85aT6HutvouOdN+dMsN0jcdshp50PSCvxDvo6aIM57LqeXimjfONUEgfqQ4IFpYWAtVixptyIRE5frMp/2A==} + engines: {node: '>=14.0.0'} + dependencies: + '@smithy/types': 2.7.0 + '@smithy/util-uri-escape': 2.0.0 + tslib: 2.6.2 + dev: false + + /@smithy/querystring-parser@2.0.15: + resolution: {integrity: sha512-jbBvoK3cc81Cj1c1TH1qMYxNQKHrYQ2DoTntN9FBbtUWcGhc+T4FP6kCKYwRLXyU4AajwGIZstvNAmIEgUUNTQ==} + engines: {node: '>=14.0.0'} + dependencies: + '@smithy/types': 2.7.0 + tslib: 2.6.2 + dev: false + + /@smithy/service-error-classification@2.0.8: + resolution: {integrity: sha512-jCw9+005im8tsfYvwwSc4TTvd29kXRFkH9peQBg5R/4DD03ieGm6v6Hpv9nIAh98GwgYg1KrztcINC1s4o7/hg==} + engines: {node: '>=14.0.0'} + dependencies: + '@smithy/types': 2.7.0 + dev: false + + /@smithy/shared-ini-file-loader@2.2.7: + resolution: {integrity: sha512-0Qt5CuiogIuvQIfK+be7oVHcPsayLgfLJGkPlbgdbl0lD28nUKu4p11L+UG3SAEsqc9UsazO+nErPXw7+IgDpQ==} + engines: {node: '>=14.0.0'} + dependencies: + '@smithy/types': 2.7.0 + tslib: 2.6.2 + dev: false + + /@smithy/signature-v4@2.0.18: + resolution: {integrity: sha512-SJRAj9jT/l9ocm8D0GojMbnA1sp7I4JeStOQ4lEXI8A5eHE73vbjlzlqIFB7cLvIgau0oUl4cGVpF9IGCrvjlw==} + engines: {node: '>=14.0.0'} + dependencies: + '@smithy/eventstream-codec': 2.0.15 + '@smithy/is-array-buffer': 2.0.0 + '@smithy/types': 2.7.0 + '@smithy/util-hex-encoding': 2.0.0 + '@smithy/util-middleware': 2.0.8 + '@smithy/util-uri-escape': 2.0.0 + '@smithy/util-utf8': 2.0.2 + tslib: 2.6.2 + dev: false + + /@smithy/smithy-client@2.2.0: + resolution: {integrity: sha512-C/bkNue5H5Obgl83SnlBt4v6VM68CqIjIELh3vAabud87xFYznLNKtj6Qb69Z+QOnLp9T+We++sEem/f2AHE+Q==} + engines: {node: '>=14.0.0'} + dependencies: + '@smithy/middleware-endpoint': 2.2.3 + '@smithy/middleware-stack': 2.0.9 + '@smithy/protocol-http': 3.0.11 + '@smithy/types': 2.7.0 + '@smithy/util-stream': 2.0.23 + tslib: 2.6.2 + dev: false + + /@smithy/types@1.2.0: + resolution: {integrity: sha512-z1r00TvBqF3dh4aHhya7nz1HhvCg4TRmw51fjMrh5do3h+ngSstt/yKlNbHeb9QxJmFbmN8KEVSWgb1bRvfEoA==} + engines: {node: '>=14.0.0'} + dependencies: + tslib: 2.6.2 + dev: false + + /@smithy/types@2.7.0: + resolution: {integrity: sha512-1OIFyhK+vOkMbu4aN2HZz/MomREkrAC/HqY5mlJMUJfGrPRwijJDTeiN8Rnj9zUaB8ogXAfIOtZrrgqZ4w7Wnw==} + engines: {node: '>=14.0.0'} + dependencies: + tslib: 2.6.2 + dev: false + + /@smithy/url-parser@2.0.15: + resolution: {integrity: sha512-sADUncUj9rNbOTrdDGm4EXlUs0eQ9dyEo+V74PJoULY4jSQxS+9gwEgsPYyiu8PUOv16JC/MpHonOgqP/IEDZA==} + dependencies: + '@smithy/querystring-parser': 2.0.15 + '@smithy/types': 2.7.0 + tslib: 2.6.2 + dev: false + + /@smithy/util-base64@2.0.1: + resolution: {integrity: sha512-DlI6XFYDMsIVN+GH9JtcRp3j02JEVuWIn/QOZisVzpIAprdsxGveFed0bjbMRCqmIFe8uetn5rxzNrBtIGrPIQ==} + engines: {node: '>=14.0.0'} + dependencies: + '@smithy/util-buffer-from': 2.0.0 + tslib: 2.6.2 + dev: false + + /@smithy/util-body-length-browser@2.0.1: + resolution: {integrity: sha512-NXYp3ttgUlwkaug4bjBzJ5+yIbUbUx8VsSLuHZROQpoik+gRkIBeEG9MPVYfvPNpuXb/puqodeeUXcKFe7BLOQ==} + dependencies: + tslib: 2.6.2 + dev: false + + /@smithy/util-body-length-node@2.1.0: + resolution: {integrity: sha512-/li0/kj/y3fQ3vyzn36NTLGmUwAICb7Jbe/CsWCktW363gh1MOcpEcSO3mJ344Gv2dqz8YJCLQpb6hju/0qOWw==} + engines: {node: '>=14.0.0'} + dependencies: + tslib: 2.6.2 + dev: false + + /@smithy/util-buffer-from@2.0.0: + resolution: {integrity: sha512-/YNnLoHsR+4W4Vf2wL5lGv0ksg8Bmk3GEGxn2vEQt52AQaPSCuaO5PM5VM7lP1K9qHRKHwrPGktqVoAHKWHxzw==} + engines: {node: '>=14.0.0'} + dependencies: + '@smithy/is-array-buffer': 2.0.0 + tslib: 2.6.2 + dev: false + + /@smithy/util-config-provider@2.0.0: + resolution: {integrity: sha512-xCQ6UapcIWKxXHEU4Mcs2s7LcFQRiU3XEluM2WcCjjBtQkUN71Tb+ydGmJFPxMUrW/GWMgQEEGipLym4XG0jZg==} + engines: {node: '>=14.0.0'} + dependencies: + tslib: 2.6.2 + dev: false + + /@smithy/util-defaults-mode-browser@2.0.23: + resolution: {integrity: sha512-2u+7t7Wgz1jlfsf6il3pz6DIzyJHS3qrnNnmATICm00pQeqp2D4kUOYauOgKGIeKgVpwzzq8+hFQe749r3xR5w==} + engines: {node: '>= 10.0.0'} + dependencies: + '@smithy/property-provider': 2.0.16 + '@smithy/smithy-client': 2.2.0 + '@smithy/types': 2.7.0 + bowser: 2.11.0 + tslib: 2.6.2 + dev: false + + /@smithy/util-defaults-mode-node@2.0.30: + resolution: {integrity: sha512-nmcmEyRlClNprp7mBnUzfmW6HrKQK+yvl+cyXCRUoQSxRvZuLDrztV+JD+zr3qV/oirEc4Q0QNIrrhTDCE6JeA==} + engines: {node: '>= 10.0.0'} + dependencies: + '@smithy/config-resolver': 2.0.21 + '@smithy/credential-provider-imds': 2.1.4 + '@smithy/node-config-provider': 2.1.8 + '@smithy/property-provider': 2.0.16 + '@smithy/smithy-client': 2.2.0 + '@smithy/types': 2.7.0 + tslib: 2.6.2 + dev: false + + /@smithy/util-hex-encoding@2.0.0: + resolution: {integrity: sha512-c5xY+NUnFqG6d7HFh1IFfrm3mGl29lC+vF+geHv4ToiuJCBmIfzx6IeHLg+OgRdPFKDXIw6pvi+p3CsscaMcMA==} + engines: {node: '>=14.0.0'} + dependencies: + tslib: 2.6.2 + dev: false + + /@smithy/util-middleware@2.0.8: + resolution: {integrity: sha512-qkvqQjM8fRGGA8P2ydWylMhenCDP8VlkPn8kiNuFEaFz9xnUKC2irfqsBSJrfrOB9Qt6pQsI58r3zvvumhFMkw==} + engines: {node: '>=14.0.0'} + dependencies: + '@smithy/types': 2.7.0 + tslib: 2.6.2 + dev: false + + /@smithy/util-retry@2.0.8: + resolution: {integrity: sha512-cQTPnVaVFMjjS6cb44WV2yXtHVyXDC5icKyIbejMarJEApYeJWpBU3LINTxHqp/tyLI+MZOUdosr2mZ3sdziNg==} + engines: {node: '>= 14.0.0'} + dependencies: + '@smithy/service-error-classification': 2.0.8 + '@smithy/types': 2.7.0 + tslib: 2.6.2 + dev: false + + /@smithy/util-stream@2.0.23: + resolution: {integrity: sha512-OJMWq99LAZJUzUwTk+00plyxX3ESktBaGPhqNIEVab+53gLULiWN9B/8bRABLg0K6R6Xg4t80uRdhk3B/LZqMQ==} + engines: {node: '>=14.0.0'} + dependencies: + '@smithy/fetch-http-handler': 2.3.1 + '@smithy/node-http-handler': 2.2.1 + '@smithy/types': 2.7.0 + '@smithy/util-base64': 2.0.1 + '@smithy/util-buffer-from': 2.0.0 + '@smithy/util-hex-encoding': 2.0.0 + '@smithy/util-utf8': 2.0.2 + tslib: 2.6.2 + dev: false + + /@smithy/util-uri-escape@2.0.0: + resolution: {integrity: sha512-ebkxsqinSdEooQduuk9CbKcI+wheijxEb3utGXkCoYQkJnwTnLbH1JXGimJtUkQwNQbsbuYwG2+aFVyZf5TLaw==} + engines: {node: '>=14.0.0'} + dependencies: + tslib: 2.6.2 + dev: false + + /@smithy/util-utf8@2.0.2: + resolution: {integrity: sha512-qOiVORSPm6Ce4/Yu6hbSgNHABLP2VMv8QOC3tTDNHHlWY19pPyc++fBTbZPtx6egPXi4HQxKDnMxVxpbtX2GoA==} + engines: {node: '>=14.0.0'} + dependencies: + '@smithy/util-buffer-from': 2.0.0 + tslib: 2.6.2 + dev: false + + /@smithy/util-waiter@2.0.15: + resolution: {integrity: sha512-9Y+btzzB7MhLADW7xgD6SjvmoYaRkrb/9SCbNGmNdfO47v38rxb90IGXyDtAK0Shl9bMthTmLgjlfYc+vtz2Qw==} + engines: {node: '>=14.0.0'} + dependencies: + '@smithy/abort-controller': 2.0.15 + '@smithy/types': 2.7.0 + tslib: 2.6.2 + dev: false + + /@snowplow/node-tracker@3.19.0: + resolution: {integrity: sha512-fsMI4SRY/E8KkDZ/hqw0I/g8gGwy+7GU1N14taxyC3OtpuiPhfGp3OFuwLdsfc4Av9UpVnv7zvhfuAB7+51Uhg==} + dependencies: + '@snowplow/tracker-core': 3.19.0 + got: 11.8.6 + tslib: 2.6.2 + dev: false + + /@snowplow/tracker-core@3.19.0: + resolution: {integrity: sha512-DYxSm22QeeHg56kG8qziE/0EJIqkyOO8DM6rAjwUo5Etm2KQuKdxpPd8FyXVwOF1RtVzlq1uEkcBG8GGGjT4Aw==} + dependencies: + tslib: 2.6.2 + uuid: 3.4.0 + dev: false + + /@szmarczak/http-timer@4.0.6: + resolution: {integrity: sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==} + engines: {node: '>=10'} + dependencies: + defer-to-connect: 2.0.1 + dev: false + + /@tsconfig/node10@1.0.9: + resolution: {integrity: sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==} + + /@tsconfig/node12@1.0.11: + resolution: {integrity: sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==} + + /@tsconfig/node14@1.0.3: + resolution: {integrity: sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==} + + /@tsconfig/node16@1.0.4: + resolution: {integrity: sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==} + + /@types/aws-lambda@8.10.119: + resolution: {integrity: sha512-Vqm22aZrCvCd6I5g1SvpW151jfqwTzEZ7XJ3yZ6xaZG31nUEOEyzzVImjRcsN8Wi/QyPxId/x8GTtgIbsy8kEw==} + + /@types/babel__core@7.20.5: + resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} + dependencies: + '@babel/parser': 7.23.6 + '@babel/types': 7.23.6 + '@types/babel__generator': 7.6.8 + '@types/babel__template': 7.4.4 + '@types/babel__traverse': 7.20.4 + + /@types/babel__generator@7.6.8: + resolution: {integrity: sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==} + dependencies: + '@babel/types': 7.23.6 + + /@types/babel__template@7.4.4: + resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==} + dependencies: + '@babel/parser': 7.23.6 + '@babel/types': 7.23.6 + + /@types/babel__traverse@7.20.4: + resolution: {integrity: sha512-mSM/iKUk5fDDrEV/e83qY+Cr3I1+Q3qqTuEn++HAWYjEa1+NxZr6CNrcJGf2ZTnq4HoFGC3zaTPZTobCzCFukA==} + dependencies: + '@babel/types': 7.23.6 + + /@types/babel__traverse@7.20.5: + resolution: {integrity: sha512-WXCyOcRtH37HAUkpXhUduaxdm82b4GSlyTqajXviN4EfiuPgNYR109xMCKvpl6zPIpua0DGlMEDCq+g8EdoheQ==} + dependencies: + '@babel/types': 7.23.6 + + /@types/body-parser@1.19.5: + resolution: {integrity: sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==} + dependencies: + '@types/connect': 3.4.38 + '@types/node': 20.10.8 + dev: false + + /@types/cacheable-request@6.0.3: + resolution: {integrity: sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==} + dependencies: + '@types/http-cache-semantics': 4.0.4 + '@types/keyv': 3.1.4 + '@types/node': 20.10.8 + '@types/responselike': 1.0.3 + dev: false + + /@types/chai-as-promised@7.1.5: + resolution: {integrity: sha512-jStwss93SITGBwt/niYrkf2C+/1KTeZCZl1LaeezTlqppAKeoQC7jxyqYuP72sxBGKCIbw7oHgbYssIRzT5FCQ==} + dependencies: + '@types/chai': 4.3.4 + dev: true + + /@types/chai-spies@1.0.3: + resolution: {integrity: sha512-RBZjhVuK7vrg4rWMt04UF5zHYwfHnpk5mIWu3nQvU3AKGDixXzSjZ6v0zke6pBcaJqMv3IBZ5ibLWPMRDL0sLw==} + dependencies: + '@types/chai': 4.3.4 + dev: true + + /@types/chai@4.3.4: + resolution: {integrity: sha512-KnRanxnpfpjUTqTCXslZSEdLfXExwgNxYPdiO2WGUj8+HDjFi8R3k5RVKPeSCzLjCcshCAtVO2QBbVuAV4kTnw==} + dev: true + + /@types/connect@3.4.38: + resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==} + dependencies: + '@types/node': 20.10.8 + dev: false + + /@types/estree@1.0.5: + resolution: {integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==} + dev: false + + /@types/express-serve-static-core@4.17.41: + resolution: {integrity: sha512-OaJ7XLaelTgrvlZD8/aa0vvvxZdUmlCn6MtWeB7TkiKW70BQLc9XEPpDLPdbo52ZhXUCrznlWdCHWxJWtdyajA==} + dependencies: + '@types/node': 20.10.8 + '@types/qs': 6.9.11 + '@types/range-parser': 1.2.7 + '@types/send': 0.17.4 + dev: false + + /@types/express@4.17.21: + resolution: {integrity: sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==} + dependencies: + '@types/body-parser': 1.19.5 + '@types/express-serve-static-core': 4.17.41 + '@types/qs': 6.9.11 + '@types/serve-static': 1.15.5 + dev: false + + /@types/graceful-fs@4.1.9: + resolution: {integrity: sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==} + dependencies: + '@types/node': 20.10.8 + + /@types/http-cache-semantics@4.0.4: + resolution: {integrity: sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==} + dev: false + + /@types/http-errors@2.0.4: + resolution: {integrity: sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==} + dev: false + + /@types/istanbul-lib-coverage@2.0.6: + resolution: {integrity: sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==} + + /@types/istanbul-lib-report@3.0.3: + resolution: {integrity: sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==} + dependencies: + '@types/istanbul-lib-coverage': 2.0.6 + + /@types/istanbul-reports@3.0.4: + resolution: {integrity: sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==} + dependencies: + '@types/istanbul-lib-report': 3.0.3 + + /@types/jest@28.1.7: + resolution: {integrity: sha512-acDN4VHD40V24tgu0iC44jchXavRNVFXQ/E6Z5XNsswgoSO/4NgsXoEYmPUGookKldlZQyIpmrEXsHI9cA3ZTA==} + dependencies: + expect: 28.1.3 + pretty-format: 28.1.3 + dev: true + + /@types/jest@29.5.11: + resolution: {integrity: sha512-S2mHmYIVe13vrm6q4kN6fLYYAka15ALQki/vgDC3mIukEOx8WJlv0kQPM+d4w8Gp6u0uSdKND04IlTXBv0rwnQ==} + dependencies: + expect: 29.7.0 + pretty-format: 29.7.0 + dev: true + + /@types/json-schema@7.0.15: + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + + /@types/json5@0.0.29: + resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} + dev: true + + /@types/keyv@3.1.4: + resolution: {integrity: sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==} + dependencies: + '@types/node': 20.10.8 + dev: false + + /@types/lodash@4.14.182: + resolution: {integrity: sha512-/THyiqyQAP9AfARo4pF+aCGcyiQ94tX/Is2I7HofNRqoYLgN1PBoOWu2/zTA5zMxzP5EFutMtWtGAFRKUe961Q==} + dev: true + + /@types/long@4.0.2: + resolution: {integrity: sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==} + dev: false + + /@types/mime@1.3.5: + resolution: {integrity: sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==} + dev: false + + /@types/mime@3.0.4: + resolution: {integrity: sha512-iJt33IQnVRkqeqC7PzBHPTC6fDlRNRW8vjrgqtScAhrmMwe8c4Eo7+fUGTa+XdWrpEgpyKWMYmi2dIwMAYRzPw==} + dev: false + + /@types/minimist@1.2.5: + resolution: {integrity: sha512-hov8bUuiLiyFPGyFPE1lwWhmzYbirOXQNNo40+y3zow8aFVTeyn3VWL0VFFfdNddA8S4Vf0Tc062rzyNr7Paag==} + dev: false + + /@types/mute-stream@0.0.1: + resolution: {integrity: sha512-0yQLzYhCqGz7CQPE3iDmYjhb7KMBFOP+tBkyw+/Y2YyDI5wpS7itXXxneN1zSsUwWx3Ji6YiVYrhAnpQGS/vkw==} + dependencies: + '@types/node': 20.10.8 + dev: false + + /@types/mute-stream@0.0.4: + resolution: {integrity: sha512-CPM9nzrCPPJHQNA9keH9CVkVI+WR5kMa+7XEs5jcGQ0VoAGnLv242w8lIVgwAEfmE4oufJRaTc9PNLQl0ioAow==} + dependencies: + '@types/node': 20.10.8 + dev: false + + /@types/node-fetch@2.6.10: + resolution: {integrity: sha512-PPpPK6F9ALFTn59Ka3BaL+qGuipRfxNE8qVgkp0bVixeiR2c2/L+IVOiBdu9JhhT22sWnQEp6YyHGI2b2+CMcA==} + dependencies: + '@types/node': 20.10.8 + form-data: 4.0.0 + dev: false + + /@types/node@18.19.4: + resolution: {integrity: sha512-xNzlUhzoHotIsnFoXmJB+yWmBvFZgKCI9TtPIEdYIMM1KWfwuY8zh7wvc1u1OAXlC7dlf6mZVx/s+Y5KfFz19A==} + dependencies: + undici-types: 5.26.5 + dev: false + + /@types/node@20.10.8: + resolution: {integrity: sha512-f8nQs3cLxbAFc00vEU59yf9UyGUftkPaLGfvbVOIDdx2i1b8epBqj2aNGyP19fiyXWvlmZ7qC1XLjAzw/OKIeA==} + dependencies: + undici-types: 5.26.5 + + /@types/normalize-package-data@2.4.4: + resolution: {integrity: sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==} + + /@types/prettier@2.7.3: + resolution: {integrity: sha512-+68kP9yzs4LMp7VNh8gdzMSPZFL44MLGqiHWvttYJe+6qnuVr4Ek9wSBQoveqY/r+LwjCcU29kNVkidwim+kYA==} + dev: true + + /@types/qs@6.9.11: + resolution: {integrity: sha512-oGk0gmhnEJK4Yyk+oI7EfXsLayXatCWPHary1MtcmbAifkobT9cM9yutG/hZKIseOU0MqbIwQ/u2nn/Gb+ltuQ==} + dev: false + + /@types/range-parser@1.2.7: + resolution: {integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==} + dev: false + + /@types/responselike@1.0.3: + resolution: {integrity: sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==} + dependencies: + '@types/node': 20.10.8 + dev: false + + /@types/semver@7.5.6: + resolution: {integrity: sha512-dn1l8LaMea/IjDoHNd9J52uBbInB796CDffS6VdIxvqYCPSG0V0DzHp76GpaWnlhg88uYyPbXCDIowa86ybd5A==} + + /@types/send@0.17.4: + resolution: {integrity: sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==} + dependencies: + '@types/mime': 1.3.5 + '@types/node': 20.10.8 + dev: false + + /@types/serve-static@1.15.5: + resolution: {integrity: sha512-PDRk21MnK70hja/YF8AHfC7yIsiQHn1rcXx7ijCFBX/k+XQJhQT/gw3xekXKJvx+5SXaMMS8oqQy09Mzvz2TuQ==} + dependencies: + '@types/http-errors': 2.0.4 + '@types/mime': 3.0.4 + '@types/node': 20.10.8 + dev: false + + /@types/stack-utils@2.0.3: + resolution: {integrity: sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==} + + /@types/triple-beam@1.3.5: + resolution: {integrity: sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==} + dev: false + + /@types/uuid@9.0.7: + resolution: {integrity: sha512-WUtIVRUZ9i5dYXefDEAI7sh9/O7jGvHg7Df/5O/gtH3Yabe5odI3UWopVR1qbPXQtvOxWu3mM4XxlYeZtMWF4g==} + dev: false + + /@types/wrap-ansi@3.0.0: + resolution: {integrity: sha512-ltIpx+kM7g/MLRZfkbL7EsCEjfzCcScLpkg37eXEtx5kmrAKBkTJwd1GIAjDSL8wTpM6Hzn5YO4pSb91BEwu1g==} + dev: false + + /@types/yargs-parser@21.0.3: + resolution: {integrity: sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==} + + /@types/yargs@17.0.32: + resolution: {integrity: sha512-xQ67Yc/laOG5uMfX/093MRlGGCIBzZMarVa+gfNKJxWAIgykYpVGkBdbqEzGDDfCrVUj6Hiff4mTZ5BA6TmAog==} + dependencies: + '@types/yargs-parser': 21.0.3 + + /@types/yauzl@2.10.3: + resolution: {integrity: sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==} + requiresBuild: true + dependencies: + '@types/node': 20.10.8 + dev: false + optional: true + + /@types/yoga-layout@1.9.2: + resolution: {integrity: sha512-S9q47ByT2pPvD65IvrWp7qppVMpk9WGMbVq9wbWZOHg6tnXSD4vyhao6nOSBwwfDdV2p3Kx9evA9vI+XWTfDvw==} + dev: false + + /@typescript-eslint/eslint-plugin@5.62.0(@typescript-eslint/parser@5.62.0)(eslint@8.56.0)(typescript@5.3.3): + resolution: {integrity: sha512-TiZzBSJja/LbhNPvk6yc0JrX9XqhQ0hdh6M2svYfsHGejaKFIAGd9MQ+ERIMzLGlN/kZoYIgdxFV0PuljTKXag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + '@typescript-eslint/parser': ^5.0.0 + eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@eslint-community/regexpp': 4.10.0 + '@typescript-eslint/parser': 5.62.0(eslint@8.56.0)(typescript@5.3.3) + '@typescript-eslint/scope-manager': 5.62.0 + '@typescript-eslint/type-utils': 5.62.0(eslint@8.56.0)(typescript@5.3.3) + '@typescript-eslint/utils': 5.62.0(eslint@8.56.0)(typescript@5.3.3) + debug: 4.3.4 + eslint: 8.56.0 + graphemer: 1.4.0 + ignore: 5.3.0 + natural-compare-lite: 1.4.0 + semver: 7.5.4 + tsutils: 3.21.0(typescript@5.3.3) + typescript: 5.3.3 + transitivePeerDependencies: + - supports-color + dev: true + + /@typescript-eslint/eslint-plugin@6.1.0(@typescript-eslint/parser@6.1.0)(eslint@8.45.0)(typescript@5.3.3): + resolution: {integrity: sha512-qg7Bm5TyP/I7iilGyp6DRqqkt8na00lI6HbjWZObgk3FFSzH5ypRwAHXJhJkwiRtTcfn+xYQIMOR5kJgpo6upw==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + '@typescript-eslint/parser': ^6.0.0 || ^6.0.0-alpha + eslint: ^7.0.0 || ^8.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@eslint-community/regexpp': 4.10.0 + '@typescript-eslint/parser': 6.1.0(eslint@8.45.0)(typescript@5.3.3) + '@typescript-eslint/scope-manager': 6.1.0 + '@typescript-eslint/type-utils': 6.1.0(eslint@8.45.0)(typescript@5.3.3) + '@typescript-eslint/utils': 6.1.0(eslint@8.45.0)(typescript@5.3.3) + '@typescript-eslint/visitor-keys': 6.1.0 + debug: 4.3.4 + eslint: 8.45.0 + graphemer: 1.4.0 + ignore: 5.3.0 + natural-compare: 1.4.0 + natural-compare-lite: 1.4.0 + semver: 7.5.4 + ts-api-utils: 1.0.3(typescript@5.3.3) + typescript: 5.3.3 + transitivePeerDependencies: + - supports-color + dev: false /@typescript-eslint/eslint-plugin@6.1.0(@typescript-eslint/parser@6.1.0)(eslint@8.56.0)(typescript@5.3.3): resolution: {integrity: sha512-qg7Bm5TyP/I7iilGyp6DRqqkt8na00lI6HbjWZObgk3FFSzH5ypRwAHXJhJkwiRtTcfn+xYQIMOR5kJgpo6upw==} engines: {node: ^16.0.0 || >=18.0.0} peerDependencies: - '@typescript-eslint/parser': ^6.0.0 || ^6.0.0-alpha - eslint: ^7.0.0 || ^8.0.0 - typescript: '*' + '@typescript-eslint/parser': ^6.0.0 || ^6.0.0-alpha + eslint: ^7.0.0 || ^8.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@eslint-community/regexpp': 4.10.0 + '@typescript-eslint/parser': 6.1.0(eslint@8.56.0)(typescript@5.3.3) + '@typescript-eslint/scope-manager': 6.1.0 + '@typescript-eslint/type-utils': 6.1.0(eslint@8.56.0)(typescript@5.3.3) + '@typescript-eslint/utils': 6.1.0(eslint@8.56.0)(typescript@5.3.3) + '@typescript-eslint/visitor-keys': 6.1.0 + debug: 4.3.4 + eslint: 8.56.0 + graphemer: 1.4.0 + ignore: 5.3.0 + natural-compare: 1.4.0 + natural-compare-lite: 1.4.0 + semver: 7.5.4 + ts-api-utils: 1.0.3(typescript@5.3.3) + typescript: 5.3.3 + transitivePeerDependencies: + - supports-color + dev: true + + /@typescript-eslint/parser@5.62.0(eslint@8.56.0)(typescript@5.3.3): + resolution: {integrity: sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@typescript-eslint/scope-manager': 5.62.0 + '@typescript-eslint/types': 5.62.0 + '@typescript-eslint/typescript-estree': 5.62.0(typescript@5.3.3) + debug: 4.3.4 + eslint: 8.56.0 + typescript: 5.3.3 + transitivePeerDependencies: + - supports-color + dev: true + + /@typescript-eslint/parser@6.1.0(eslint@8.45.0)(typescript@5.3.3): + resolution: {integrity: sha512-hIzCPvX4vDs4qL07SYzyomamcs2/tQYXg5DtdAfj35AyJ5PIUqhsLf4YrEIFzZcND7R2E8tpQIZKayxg8/6Wbw==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + eslint: ^7.0.0 || ^8.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@typescript-eslint/scope-manager': 6.1.0 + '@typescript-eslint/types': 6.1.0 + '@typescript-eslint/typescript-estree': 6.1.0(typescript@5.3.3) + '@typescript-eslint/visitor-keys': 6.1.0 + debug: 4.3.4 + eslint: 8.45.0 + typescript: 5.3.3 + transitivePeerDependencies: + - supports-color + dev: false + + /@typescript-eslint/parser@6.1.0(eslint@8.56.0)(typescript@5.3.3): + resolution: {integrity: sha512-hIzCPvX4vDs4qL07SYzyomamcs2/tQYXg5DtdAfj35AyJ5PIUqhsLf4YrEIFzZcND7R2E8tpQIZKayxg8/6Wbw==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + eslint: ^7.0.0 || ^8.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@typescript-eslint/scope-manager': 6.1.0 + '@typescript-eslint/types': 6.1.0 + '@typescript-eslint/typescript-estree': 6.1.0(typescript@5.3.3) + '@typescript-eslint/visitor-keys': 6.1.0 + debug: 4.3.4 + eslint: 8.56.0 + typescript: 5.3.3 + transitivePeerDependencies: + - supports-color + dev: true + + /@typescript-eslint/scope-manager@5.62.0: + resolution: {integrity: sha512-VXuvVvZeQCQb5Zgf4HAxc04q5j+WrNAtNh9OwCsCgpKqESMTu3tF/jhZ3xG6T4NZwWl65Bg8KuS2uEvhSfLl0w==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dependencies: + '@typescript-eslint/types': 5.62.0 + '@typescript-eslint/visitor-keys': 5.62.0 + dev: true + + /@typescript-eslint/scope-manager@6.1.0: + resolution: {integrity: sha512-AxjgxDn27hgPpe2rQe19k0tXw84YCOsjDJ2r61cIebq1t+AIxbgiXKvD4999Wk49GVaAcdJ/d49FYel+Pp3jjw==} + engines: {node: ^16.0.0 || >=18.0.0} + dependencies: + '@typescript-eslint/types': 6.1.0 + '@typescript-eslint/visitor-keys': 6.1.0 + + /@typescript-eslint/type-utils@5.62.0(eslint@8.56.0)(typescript@5.3.3): + resolution: {integrity: sha512-xsSQreu+VnfbqQpW5vnCJdq1Z3Q0U31qiWmRhr98ONQmcp/yhiPJFPq8MXiJVLiksmOKSjIldZzkebzHuCGzew==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: '*' + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@typescript-eslint/typescript-estree': 5.62.0(typescript@5.3.3) + '@typescript-eslint/utils': 5.62.0(eslint@8.56.0)(typescript@5.3.3) + debug: 4.3.4 + eslint: 8.56.0 + tsutils: 3.21.0(typescript@5.3.3) + typescript: 5.3.3 + transitivePeerDependencies: + - supports-color + dev: true + + /@typescript-eslint/type-utils@6.1.0(eslint@8.45.0)(typescript@5.3.3): + resolution: {integrity: sha512-kFXBx6QWS1ZZ5Ni89TyT1X9Ag6RXVIVhqDs0vZE/jUeWlBv/ixq2diua6G7ece6+fXw3TvNRxP77/5mOMusx2w==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + eslint: ^7.0.0 || ^8.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@typescript-eslint/typescript-estree': 6.1.0(typescript@5.3.3) + '@typescript-eslint/utils': 6.1.0(eslint@8.45.0)(typescript@5.3.3) + debug: 4.3.4 + eslint: 8.45.0 + ts-api-utils: 1.0.3(typescript@5.3.3) + typescript: 5.3.3 + transitivePeerDependencies: + - supports-color + dev: false + + /@typescript-eslint/type-utils@6.1.0(eslint@8.56.0)(typescript@5.3.3): + resolution: {integrity: sha512-kFXBx6QWS1ZZ5Ni89TyT1X9Ag6RXVIVhqDs0vZE/jUeWlBv/ixq2diua6G7ece6+fXw3TvNRxP77/5mOMusx2w==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + eslint: ^7.0.0 || ^8.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@typescript-eslint/typescript-estree': 6.1.0(typescript@5.3.3) + '@typescript-eslint/utils': 6.1.0(eslint@8.56.0)(typescript@5.3.3) + debug: 4.3.4 + eslint: 8.56.0 + ts-api-utils: 1.0.3(typescript@5.3.3) + typescript: 5.3.3 + transitivePeerDependencies: + - supports-color + dev: true + + /@typescript-eslint/types@5.62.0: + resolution: {integrity: sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dev: true + + /@typescript-eslint/types@6.1.0: + resolution: {integrity: sha512-+Gfd5NHCpDoHDOaU/yIF3WWRI2PcBRKKpP91ZcVbL0t5tQpqYWBs3z/GGhvU+EV1D0262g9XCnyqQh19prU0JQ==} + engines: {node: ^16.0.0 || >=18.0.0} + + /@typescript-eslint/typescript-estree@5.62.0(typescript@5.3.3): + resolution: {integrity: sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@typescript-eslint/types': 5.62.0 + '@typescript-eslint/visitor-keys': 5.62.0 + debug: 4.3.4 + globby: 11.1.0 + is-glob: 4.0.3 + semver: 7.5.4 + tsutils: 3.21.0(typescript@5.3.3) + typescript: 5.3.3 + transitivePeerDependencies: + - supports-color + dev: true + + /@typescript-eslint/typescript-estree@6.1.0(typescript@5.3.3): + resolution: {integrity: sha512-nUKAPWOaP/tQjU1IQw9sOPCDavs/iU5iYLiY/6u7gxS7oKQoi4aUxXS1nrrVGTyBBaGesjkcwwHkbkiD5eBvcg==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@typescript-eslint/types': 6.1.0 + '@typescript-eslint/visitor-keys': 6.1.0 + debug: 4.3.4 + globby: 11.1.0 + is-glob: 4.0.3 + semver: 7.5.4 + ts-api-utils: 1.0.3(typescript@5.3.3) + typescript: 5.3.3 + transitivePeerDependencies: + - supports-color + + /@typescript-eslint/utils@5.62.0(eslint@8.56.0)(typescript@5.3.3): + resolution: {integrity: sha512-n8oxjeb5aIbPFEtmQxQYOLI0i9n5ySBEY/ZEHHZqKQSFnxio1rv6dthascc9dLuwrL0RC5mPCxB7vnAVGAYWAQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 + dependencies: + '@eslint-community/eslint-utils': 4.4.0(eslint@8.56.0) + '@types/json-schema': 7.0.15 + '@types/semver': 7.5.6 + '@typescript-eslint/scope-manager': 5.62.0 + '@typescript-eslint/types': 5.62.0 + '@typescript-eslint/typescript-estree': 5.62.0(typescript@5.3.3) + eslint: 8.56.0 + eslint-scope: 5.1.1 + semver: 7.5.4 + transitivePeerDependencies: + - supports-color + - typescript + dev: true + + /@typescript-eslint/utils@6.1.0(eslint@8.45.0)(typescript@5.3.3): + resolution: {integrity: sha512-wp652EogZlKmQoMS5hAvWqRKplXvkuOnNzZSE0PVvsKjpexd/XznRVHAtrfHFYmqaJz0DFkjlDsGYC9OXw+OhQ==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + eslint: ^7.0.0 || ^8.0.0 + dependencies: + '@eslint-community/eslint-utils': 4.4.0(eslint@8.45.0) + '@types/json-schema': 7.0.15 + '@types/semver': 7.5.6 + '@typescript-eslint/scope-manager': 6.1.0 + '@typescript-eslint/types': 6.1.0 + '@typescript-eslint/typescript-estree': 6.1.0(typescript@5.3.3) + eslint: 8.45.0 + semver: 7.5.4 + transitivePeerDependencies: + - supports-color + - typescript + dev: false + + /@typescript-eslint/utils@6.1.0(eslint@8.56.0)(typescript@5.3.3): + resolution: {integrity: sha512-wp652EogZlKmQoMS5hAvWqRKplXvkuOnNzZSE0PVvsKjpexd/XznRVHAtrfHFYmqaJz0DFkjlDsGYC9OXw+OhQ==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + eslint: ^7.0.0 || ^8.0.0 + dependencies: + '@eslint-community/eslint-utils': 4.4.0(eslint@8.56.0) + '@types/json-schema': 7.0.15 + '@types/semver': 7.5.6 + '@typescript-eslint/scope-manager': 6.1.0 + '@typescript-eslint/types': 6.1.0 + '@typescript-eslint/typescript-estree': 6.1.0(typescript@5.3.3) + eslint: 8.56.0 + semver: 7.5.4 + transitivePeerDependencies: + - supports-color + - typescript + dev: true + + /@typescript-eslint/visitor-keys@5.62.0: + resolution: {integrity: sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dependencies: + '@typescript-eslint/types': 5.62.0 + eslint-visitor-keys: 3.4.3 + dev: true + + /@typescript-eslint/visitor-keys@6.1.0: + resolution: {integrity: sha512-yQeh+EXhquh119Eis4k0kYhj9vmFzNpbhM3LftWQVwqVjipCkwHBQOZutcYW+JVkjtTG9k8nrZU1UoNedPDd1A==} + engines: {node: ^16.0.0 || >=18.0.0} + dependencies: + '@typescript-eslint/types': 6.1.0 + eslint-visitor-keys: 3.4.3 + + /@ungap/structured-clone@1.2.0: + resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==} + dev: true + + /@vercel/style-guide@4.0.2(eslint@8.56.0)(prettier@3.0.0)(typescript@5.3.3): + resolution: {integrity: sha512-FroL+oOePzhw7n/I+f7zr4WNroGHT/+2TlW6WH9+CVSjMNsEyu7Qstj2mI5gWIBjT1Y2ZImKPppCzI2cIYmNZw==} + engines: {node: '>=16'} + peerDependencies: + '@next/eslint-plugin-next': ^12.3.0 + eslint: ^8.24.0 + prettier: ^2.7.0 + typescript: ^4.8.0 + peerDependenciesMeta: + '@next/eslint-plugin-next': + optional: true + eslint: + optional: true + prettier: + optional: true + typescript: + optional: true + dependencies: + '@babel/core': 7.23.6 + '@babel/eslint-parser': 7.23.3(@babel/core@7.23.6)(eslint@8.56.0) + '@rushstack/eslint-patch': 1.6.1 + '@typescript-eslint/eslint-plugin': 5.62.0(@typescript-eslint/parser@5.62.0)(eslint@8.56.0)(typescript@5.3.3) + '@typescript-eslint/parser': 5.62.0(eslint@8.56.0)(typescript@5.3.3) + eslint: 8.56.0 + eslint-config-prettier: 8.8.0(eslint@8.56.0) + eslint-import-resolver-alias: 1.1.2(eslint-plugin-import@2.29.1) + eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@5.62.0)(eslint-plugin-import@2.29.1)(eslint@8.56.0) + eslint-plugin-eslint-comments: 3.2.0(eslint@8.56.0) + eslint-plugin-import: 2.29.1(@typescript-eslint/parser@6.1.0)(eslint@8.56.0) + eslint-plugin-jest: 27.6.0(@typescript-eslint/eslint-plugin@5.62.0)(eslint@8.56.0)(typescript@5.3.3) + eslint-plugin-jsx-a11y: 6.8.0(eslint@8.56.0) + eslint-plugin-playwright: 0.11.2(eslint-plugin-jest@27.6.0)(eslint@8.56.0) + eslint-plugin-react: 7.33.2(eslint@8.56.0) + eslint-plugin-react-hooks: 4.6.0(eslint@8.56.0) + eslint-plugin-testing-library: 5.11.1(eslint@8.56.0)(typescript@5.3.3) + eslint-plugin-tsdoc: 0.2.17 + eslint-plugin-unicorn: 43.0.2(eslint@8.56.0) + prettier: 3.0.0 + prettier-plugin-packagejson: 2.4.8(prettier@3.0.0) + typescript: 5.3.3 + transitivePeerDependencies: + - eslint-import-resolver-node + - eslint-import-resolver-webpack + - jest + - supports-color + dev: true + + /@wry/context@0.7.4: + resolution: {integrity: sha512-jmT7Sb4ZQWI5iyu3lobQxICu2nC/vbUhP0vIdd6tHC9PTfenmRmuIFqktc6GH9cgi+ZHnsLWPvfSvc4DrYmKiQ==} + engines: {node: '>=8'} + dependencies: + tslib: 2.6.2 + dev: false + + /@wry/equality@0.5.7: + resolution: {integrity: sha512-BRFORjsTuQv5gxcXsuDXx6oGRhuVsEGwZy6LOzRRfgu+eSfxbhUQ9L9YtSEIuIjY/o7g3iWFjrc5eSY1GXP2Dw==} + engines: {node: '>=8'} + dependencies: + tslib: 2.6.2 + dev: false + + /@wry/trie@0.3.2: + resolution: {integrity: sha512-yRTyhWSls2OY/pYLfwff867r8ekooZ4UI+/gxot5Wj8EFwSf2rG+n+Mo/6LoLQm1TKA4GRj2+LCpbfS937dClQ==} + engines: {node: '>=8'} + dependencies: + tslib: 2.6.2 + dev: false + + /@wry/trie@0.4.3: + resolution: {integrity: sha512-I6bHwH0fSf6RqQcnnXLJKhkSXG45MFral3GxPaY4uAl0LYDZM+YDVDAiU9bYwjTuysy1S0IeecWtmq1SZA3M1w==} + engines: {node: '>=8'} + dependencies: + tslib: 2.6.2 + dev: false + + /@xmldom/xmldom@0.8.10: + resolution: {integrity: sha512-2WALfTl4xo2SkGCYRt6rDTFfk9R1czmBvUQy12gK2KuRKIpWEhcbbzy8EZXtz/jkRqHX8bFEc6FC1HjX4TUWYw==} + engines: {node: '>=10.0.0'} + dev: false + + /JSONStream@1.3.5: + resolution: {integrity: sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==} + hasBin: true + dependencies: + jsonparse: 1.3.1 + through: 2.3.8 + dev: false + + /abbrev@1.1.1: + resolution: {integrity: sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==} + dev: true + + /accepts@1.3.8: + resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} + engines: {node: '>= 0.6'} + dependencies: + mime-types: 2.1.35 + negotiator: 0.6.3 + dev: false + + /acorn-jsx@5.3.2(acorn@8.11.3): + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + dependencies: + acorn: 8.11.3 + + /acorn-walk@8.3.1: + resolution: {integrity: sha512-TgUZgYvqZprrl7YldZNoa9OciCAyZR+Ejm9eXzKCmjsF5IKp/wgQ7Z/ZpjpGTIUPwrHQIcYeI8qDh4PsEwxMbw==} + engines: {node: '>=0.4.0'} + + /acorn@8.11.3: + resolution: {integrity: sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==} + engines: {node: '>=0.4.0'} + hasBin: true + + /address@1.2.2: + resolution: {integrity: sha512-4B/qKCfeE/ODUaAUpSwfzazo5x29WD4r3vXiWsB7I2mSDAihwEqKO+g8GELZUQSSAo5e1XTYh3ZVfLyxBc12nA==} + engines: {node: '>= 10.0.0'} + dev: false + + /agent-base@6.0.2: + resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} + engines: {node: '>= 6.0.0'} + dependencies: + debug: 4.3.4 + transitivePeerDependencies: + - supports-color + dev: false + + /agent-base@7.1.0: + resolution: {integrity: sha512-o/zjMZRhJxny7OyEF+Op8X+efiELC7k7yOjMzgfzVqOzXqkBkWI79YoTdOtsuWd5BWhAGAuOY/Xa6xpiaWXiNg==} + engines: {node: '>= 14'} + dependencies: + debug: 4.3.4 + transitivePeerDependencies: + - supports-color + dev: false + + /aggregate-error@3.1.0: + resolution: {integrity: sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==} + engines: {node: '>=8'} + dependencies: + clean-stack: 2.2.0 + indent-string: 4.0.0 + dev: false + + /aggregate-error@5.0.0: + resolution: {integrity: sha512-gOsf2YwSlleG6IjRYG2A7k0HmBMEo6qVNk9Bp/EaLgAJT5ngH6PXbqa4ItvnEwCm/velL5jAnQgsHsWnjhGmvw==} + engines: {node: '>=18'} + dependencies: + clean-stack: 5.2.0 + indent-string: 5.0.0 + dev: false + + /ajv@6.12.6: + resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + + /ajv@8.12.0: + resolution: {integrity: sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==} + dependencies: + fast-deep-equal: 3.1.3 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + uri-js: 4.4.1 + dev: false + + /ansi-escapes@4.3.2: + resolution: {integrity: sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==} + engines: {node: '>=8'} + dependencies: + type-fest: 0.21.3 + + /ansi-escapes@6.2.0: + resolution: {integrity: sha512-kzRaCqXnpzWs+3z5ABPQiVke+iq0KXkHo8xiWV4RPTi5Yli0l97BEQuhXV1s7+aSU/fu1kUuxgS4MsQ0fRuygw==} + engines: {node: '>=14.16'} + dependencies: + type-fest: 3.13.1 + dev: false + + /ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + /ansi-regex@6.0.1: + resolution: {integrity: sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==} + engines: {node: '>=12'} + dev: false + + /ansi-styles@3.2.1: + resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==} + engines: {node: '>=4'} + dependencies: + color-convert: 1.9.3 + + /ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + dependencies: + color-convert: 2.0.1 + + /ansi-styles@5.2.0: + resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} + engines: {node: '>=10'} + + /ansi-styles@6.2.1: + resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==} + engines: {node: '>=12'} + dev: false + + /ansicolors@0.3.2: + resolution: {integrity: sha512-QXu7BPrP29VllRxH8GwB7x5iX5qWKAAMLqKQGWTeLWVlNHNOpVMJ91dsxQAIWXpjuW5wqvxu3Jd/nRjrJ+0pqg==} + dev: false + + /any-promise@1.3.0: + resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} + dev: false + + /anymatch@3.1.3: + resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} + engines: {node: '>= 8'} + dependencies: + normalize-path: 3.0.0 + picomatch: 2.3.1 + + /apollo-server-cache-redis@3.3.1: + resolution: {integrity: sha512-z+WX74PKM8Epsyd+cjRq789b2RtgARcdQ/u8acs+T5nf32Qt7eYhumKemUZf7lkwnKfh0nCNf81+e1Ll4xckAA==} + engines: {node: '>=12.0'} + deprecated: This package is part of the legacy caching implementation used by Apollo Server v2 and v3, and is no longer maintained. We recommend you switch to the newer Keyv-based implementation (which is compatible with all versions of Apollo Server). See https://www.apollographql.com/docs/apollo-server/v3/performance/cache-backends#legacy-caching-implementation for more details. + dependencies: + apollo-server-caching: 3.3.0 + apollo-server-env: 4.2.1 + dataloader: 2.2.2 + ioredis: 4.28.5 + transitivePeerDependencies: + - encoding + - supports-color + dev: false + + /apollo-server-caching@3.3.0: + resolution: {integrity: sha512-Wgcb0ArjZ5DjQ7ID+tvxUcZ7Yxdbk5l1MxZL8D8gkyjooOkhPNzjRVQ7ubPoXqO54PrOMOTm1ejVhsF+AfIirQ==} + engines: {node: '>=12.0'} + deprecated: This package is part of the legacy caching implementation used by Apollo Server v2 and v3, and is no longer maintained. We recommend you switch to the newer Keyv-based implementation (which is compatible with all versions of Apollo Server). See https://www.apollographql.com/docs/apollo-server/v3/performance/cache-backends#legacy-caching-implementation for more details. + dependencies: + lru-cache: 6.0.0 + dev: false + + /apollo-server-env@4.2.1: + resolution: {integrity: sha512-vm/7c7ld+zFMxibzqZ7SSa5tBENc4B0uye9LTfjJwGoQFY5xsUPH5FpO5j0bMUDZ8YYNbrF9SNtzc5Cngcr90g==} + engines: {node: '>=12.0'} + deprecated: The `apollo-server-env` package is part of Apollo Server v2 and v3, which are now deprecated (end-of-life October 22nd 2023 and October 22nd 2024, respectively). This package's functionality is now found in the `@apollo/utils.fetcher` package. See https://www.apollographql.com/docs/apollo-server/previous-versions/ for more details. + dependencies: + node-fetch: 2.7.0 + transitivePeerDependencies: + - encoding + dev: false + + /archiver-utils@2.1.0: + resolution: {integrity: sha512-bEL/yUb/fNNiNTuUz979Z0Yg5L+LzLxGJz8x79lYmR54fmTIb6ob/hNQgkQnIUDWIFjZVQwl9Xs356I6BAMHfw==} + engines: {node: '>= 6'} + dependencies: + glob: 7.2.3 + graceful-fs: 4.2.11 + lazystream: 1.0.1 + lodash.defaults: 4.2.0 + lodash.difference: 4.5.0 + lodash.flatten: 4.4.0 + lodash.isplainobject: 4.0.6 + lodash.union: 4.6.0 + normalize-path: 3.0.0 + readable-stream: 2.3.8 + dev: false + + /archiver-utils@3.0.4: + resolution: {integrity: sha512-KVgf4XQVrTjhyWmx6cte4RxonPLR9onExufI1jhvw/MQ4BB6IsZD5gT8Lq+u/+pRkWna/6JoHpiQioaqFP5Rzw==} + engines: {node: '>= 10'} + dependencies: + glob: 7.2.3 + graceful-fs: 4.2.11 + lazystream: 1.0.1 + lodash.defaults: 4.2.0 + lodash.difference: 4.5.0 + lodash.flatten: 4.4.0 + lodash.isplainobject: 4.0.6 + lodash.union: 4.6.0 + normalize-path: 3.0.0 + readable-stream: 3.6.2 + dev: false + + /archiver@5.3.2: + resolution: {integrity: sha512-+25nxyyznAXF7Nef3y0EbBeqmGZgeN/BxHX29Rs39djAfaFalmQ89SE6CWyDCHzGL0yt/ycBtNOmGTW0FyGWNw==} + engines: {node: '>= 10'} + dependencies: + archiver-utils: 2.1.0 + async: 3.2.5 + buffer-crc32: 0.2.13 + readable-stream: 3.6.2 + readdir-glob: 1.1.3 + tar-stream: 2.2.0 + zip-stream: 4.1.1 + dev: false + + /arg@4.1.3: + resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==} + + /argparse@1.0.10: + resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} + dependencies: + sprintf-js: 1.0.3 + + /argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + /argv-formatter@1.0.0: + resolution: {integrity: sha512-F2+Hkm9xFaRg+GkaNnbwXNDV5O6pnCFEmqyhvfC/Ic5LbgOWjJh3L+mN/s91rxVL3znE7DYVpW0GJFT+4YBgWw==} + dev: false + + /aria-query@5.3.0: + resolution: {integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==} + dependencies: + dequal: 2.0.3 + dev: true + + /arr-rotate@1.0.0: + resolution: {integrity: sha512-yOzOZcR9Tn7enTF66bqKorGGH0F36vcPaSWg8fO0c0UYb3LX3VMXj5ZxEqQLNOecAhlRJ7wYZja5i4jTlnbIfQ==} + engines: {node: '>=4'} + dev: false + + /array-buffer-byte-length@1.0.0: + resolution: {integrity: sha512-LPuwb2P+NrQw3XhxGc36+XSvuBPopovXYTR9Ew++Du9Yb/bx5AzBfrIsBoj0EZUifjQU+sHL21sseZ3jerWO/A==} + dependencies: + call-bind: 1.0.5 + is-array-buffer: 3.0.2 + + /array-flatten@1.1.1: + resolution: {integrity: sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==} + dev: false + + /array-ify@1.0.0: + resolution: {integrity: sha512-c5AMf34bKdvPhQ7tBGhqkgKNUzMr4WUs+WDtC2ZUGOUncbxKMTvqxYctiseW3+L4bA8ec+GcZ6/A/FW4m8ukng==} + dev: false + + /array-includes@3.1.7: + resolution: {integrity: sha512-dlcsNBIiWhPkHdOEEKnehA+RNUWDc4UqFtnIXU4uuYDPtA4LDkr7qip2p0VvFAEXNDr0yWZ9PJyIRiGjRLQzwQ==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.5 + define-properties: 1.2.1 + es-abstract: 1.22.3 + get-intrinsic: 1.2.2 + is-string: 1.0.7 + dev: true + + /array-union@2.1.0: + resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} + engines: {node: '>=8'} + + /array.prototype.findlastindex@1.2.3: + resolution: {integrity: sha512-LzLoiOMAxvy+Gd3BAq3B7VeIgPdo+Q8hthvKtXybMvRV0jrXfJM/t8mw7nNlpEcVlVUnCnM2KSX4XU5HmpodOA==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.5 + define-properties: 1.2.1 + es-abstract: 1.22.3 + es-shim-unscopables: 1.0.2 + get-intrinsic: 1.2.2 + dev: true + + /array.prototype.flat@1.3.2: + resolution: {integrity: sha512-djYB+Zx2vLewY8RWlNCUdHjDXs2XOgm602S9E7P/UpHgfeHL00cRiIF+IN/G/aUJ7kGPb6yO/ErDI5V2s8iycA==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.5 + define-properties: 1.2.1 + es-abstract: 1.22.3 + es-shim-unscopables: 1.0.2 + dev: true + + /array.prototype.flatmap@1.3.2: + resolution: {integrity: sha512-Ewyx0c9PmpcsByhSW4r+9zDU7sGjFc86qf/kKtuSCRdhfbk0SNLLkaT5qvcHnRGgc5NP/ly/y+qkXkqONX54CQ==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.5 + define-properties: 1.2.1 + es-abstract: 1.22.3 + es-shim-unscopables: 1.0.2 + dev: true + + /array.prototype.tosorted@1.1.2: + resolution: {integrity: sha512-HuQCHOlk1Weat5jzStICBCd83NxiIMwqDg/dHEsoefabn/hJRj5pVdWcPUSpRrwhwxZOsQassMpgN/xRYFBMIg==} + dependencies: + call-bind: 1.0.5 + define-properties: 1.2.1 + es-abstract: 1.22.3 + es-shim-unscopables: 1.0.2 + get-intrinsic: 1.2.2 + dev: true + + /arraybuffer.prototype.slice@1.0.2: + resolution: {integrity: sha512-yMBKppFur/fbHu9/6USUe03bZ4knMYiwFBcyiaXB8Go0qNehwX6inYPzK9U0NeQvGxKthcmHcaR8P5MStSRBAw==} + engines: {node: '>= 0.4'} + dependencies: + array-buffer-byte-length: 1.0.0 + call-bind: 1.0.5 + define-properties: 1.2.1 + es-abstract: 1.22.3 + get-intrinsic: 1.2.2 + is-array-buffer: 3.0.2 + is-shared-array-buffer: 1.0.2 + dev: true + + /arrify@1.0.1: + resolution: {integrity: sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA==} + engines: {node: '>=0.10.0'} + dev: false + + /asap@2.0.6: + resolution: {integrity: sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==} + dev: false + + /assertion-error@1.1.0: + resolution: {integrity: sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==} + dev: true + + /ast-types-flow@0.0.8: + resolution: {integrity: sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==} + dev: true + + /astral-regex@2.0.0: + resolution: {integrity: sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==} + engines: {node: '>=8'} + dev: false + + /async-retry@1.3.3: + resolution: {integrity: sha512-wfr/jstw9xNi/0teMHrRW7dsz3Lt5ARhYNZ2ewpadnhaIp5mbALhOAP+EAdsC7t4Z6wqsDVv9+W6gm1Dk9mEyw==} + dependencies: + retry: 0.13.1 + dev: false + + /async@3.2.5: + resolution: {integrity: sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==} + dev: false + + /asynciterator.prototype@1.0.0: + resolution: {integrity: sha512-wwHYEIS0Q80f5mosx3L/dfG5t5rjEa9Ft51GTaNt862EnpyGHpgz2RkZvLPp1oF5TnAiTohkEKVEu8pQPJI7Vg==} + dependencies: + has-symbols: 1.0.3 + dev: true + + /asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + dev: false + + /at-least-node@1.0.0: + resolution: {integrity: sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==} + engines: {node: '>= 4.0.0'} + dev: false + + /auto-bind@4.0.0: + resolution: {integrity: sha512-Hdw8qdNiqdJ8LqT0iK0sVzkFbzg6fhnQqqfWhBDxcHZvU75+B+ayzTy8x+k5Ix0Y92XOhOUlx74ps+bA6BeYMQ==} + engines: {node: '>=8'} + dev: false + + /available-typed-arrays@1.0.5: + resolution: {integrity: sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==} + engines: {node: '>= 0.4'} + + /axe-core@4.7.0: + resolution: {integrity: sha512-M0JtH+hlOL5pLQwHOLNYZaXuhqmvS8oExsqB1SBYgA4Dk7u/xx+YdGHXaK5pyUfed5mYXdlYiphWq3G8cRi5JQ==} + engines: {node: '>=4'} + dev: true + + /axobject-query@3.2.1: + resolution: {integrity: sha512-jsyHu61e6N4Vbz/v18DHwWYKK0bSWLqn47eeDSKPB7m8tqMHF9YJ+mhIk2lVteyZrY8tnSj/jHOv4YiTCuCJgg==} + dependencies: + dequal: 2.0.3 + dev: true + + /babel-jest@28.1.3(@babel/core@7.23.6): + resolution: {integrity: sha512-epUaPOEWMk3cWX0M/sPvCHHCe9fMFAa/9hXEgKP8nFfNl/jlGkE9ucq9NqkZGXLDduCJYS0UvSlPUwC0S+rH6Q==} + engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} + peerDependencies: + '@babel/core': ^7.8.0 + dependencies: + '@babel/core': 7.23.6 + '@jest/transform': 28.1.3 + '@types/babel__core': 7.20.5 + babel-plugin-istanbul: 6.1.1 + babel-preset-jest: 28.1.3(@babel/core@7.23.6) + chalk: 4.1.2 + graceful-fs: 4.2.11 + slash: 3.0.0 + transitivePeerDependencies: + - supports-color + dev: true + + /babel-jest@29.7.0(@babel/core@7.23.7): + resolution: {integrity: sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + '@babel/core': ^7.8.0 + dependencies: + '@babel/core': 7.23.7 + '@jest/transform': 29.7.0 + '@types/babel__core': 7.20.5 + babel-plugin-istanbul: 6.1.1 + babel-preset-jest: 29.6.3(@babel/core@7.23.7) + chalk: 4.1.2 + graceful-fs: 4.2.11 + slash: 3.0.0 + transitivePeerDependencies: + - supports-color + + /babel-plugin-istanbul@6.1.1: + resolution: {integrity: sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==} + engines: {node: '>=8'} + dependencies: + '@babel/helper-plugin-utils': 7.22.5 + '@istanbuljs/load-nyc-config': 1.1.0 + '@istanbuljs/schema': 0.1.3 + istanbul-lib-instrument: 5.2.1 + test-exclude: 6.0.0 + transitivePeerDependencies: + - supports-color + + /babel-plugin-jest-hoist@28.1.3: + resolution: {integrity: sha512-Ys3tUKAmfnkRUpPdpa98eYrAR0nV+sSFUZZEGuQ2EbFd1y4SOLtD5QDNHAq+bb9a+bbXvYQC4b+ID/THIMcU6Q==} + engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} + dependencies: + '@babel/template': 7.22.15 + '@babel/types': 7.23.6 + '@types/babel__core': 7.20.5 + '@types/babel__traverse': 7.20.4 + dev: true + + /babel-plugin-jest-hoist@29.6.3: + resolution: {integrity: sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@babel/template': 7.22.15 + '@babel/types': 7.23.6 + '@types/babel__core': 7.20.5 + '@types/babel__traverse': 7.20.5 + + /babel-preset-current-node-syntax@1.0.1(@babel/core@7.23.6): + resolution: {integrity: sha512-M7LQ0bxarkxQoN+vz5aJPsLBn77n8QgTFmo8WK0/44auK2xlCXrYcUxHFxgU7qW5Yzw/CjmLRK2uJzaCd7LvqQ==} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.23.6 + '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.23.6) + '@babel/plugin-syntax-bigint': 7.8.3(@babel/core@7.23.6) + '@babel/plugin-syntax-class-properties': 7.12.13(@babel/core@7.23.6) + '@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.23.6) + '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.23.6) + '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.23.6) + '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.23.6) + '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.23.6) + '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.23.6) + '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.23.6) + '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.23.6) + '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.23.6) + dev: true + + /babel-preset-current-node-syntax@1.0.1(@babel/core@7.23.7): + resolution: {integrity: sha512-M7LQ0bxarkxQoN+vz5aJPsLBn77n8QgTFmo8WK0/44auK2xlCXrYcUxHFxgU7qW5Yzw/CjmLRK2uJzaCd7LvqQ==} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.23.7 + '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.23.7) + '@babel/plugin-syntax-bigint': 7.8.3(@babel/core@7.23.7) + '@babel/plugin-syntax-class-properties': 7.12.13(@babel/core@7.23.7) + '@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.23.7) + '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.23.7) + '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.23.7) + '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.23.7) + '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.23.7) + '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.23.7) + '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.23.7) + '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.23.7) + '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.23.7) + + /babel-preset-jest@28.1.3(@babel/core@7.23.6): + resolution: {integrity: sha512-L+fupJvlWAHbQfn74coNX3zf60LXMJsezNvvx8eIh7iOR1luJ1poxYgQk1F8PYtNq/6QODDHCqsSnTFSWC491A==} + engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.23.6 + babel-plugin-jest-hoist: 28.1.3 + babel-preset-current-node-syntax: 1.0.1(@babel/core@7.23.6) + dev: true + + /babel-preset-jest@29.6.3(@babel/core@7.23.7): + resolution: {integrity: sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.23.7 + babel-plugin-jest-hoist: 29.6.3 + babel-preset-current-node-syntax: 1.0.1(@babel/core@7.23.7) + + /balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + /base64-js@1.5.1: + resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + dev: false + + /basic-auth@2.0.1: + resolution: {integrity: sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==} + engines: {node: '>= 0.8'} + dependencies: + safe-buffer: 5.1.2 + dev: false + + /before-after-hook@2.2.3: + resolution: {integrity: sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ==} + dev: false + + /binary-extensions@2.2.0: + resolution: {integrity: sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==} + engines: {node: '>=8'} + + /bl@4.1.0: + resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} + dependencies: + buffer: 5.7.1 + inherits: 2.0.4 + readable-stream: 3.6.2 + dev: false + + /body-parser@1.20.1: + resolution: {integrity: sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==} + engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + dependencies: + bytes: 3.1.2 + content-type: 1.0.5 + debug: 2.6.9 + depd: 2.0.0 + destroy: 1.2.0 + http-errors: 2.0.0 + iconv-lite: 0.4.24 + on-finished: 2.4.1 + qs: 6.11.0 + raw-body: 2.5.1 + type-is: 1.6.18 + unpipe: 1.0.0 + transitivePeerDependencies: + - supports-color + dev: false + + /body-parser@1.20.2: + resolution: {integrity: sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==} + engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + dependencies: + bytes: 3.1.2 + content-type: 1.0.5 + debug: 2.6.9 + depd: 2.0.0 + destroy: 1.2.0 + http-errors: 2.0.0 + iconv-lite: 0.4.24 + on-finished: 2.4.1 + qs: 6.11.0 + raw-body: 2.5.2 + type-is: 1.6.18 + unpipe: 1.0.0 + transitivePeerDependencies: + - supports-color + dev: false + + /bottleneck@2.19.5: + resolution: {integrity: sha512-VHiNCbI1lKdl44tGrhNfU3lup0Tj/ZBMJB5/2ZbNXRCPuRCO7ed2mgcK4r17y+KB2EfuYuRaVlwNbAeaWGSpbw==} + dev: false + + /bowser@2.11.0: + resolution: {integrity: sha512-AlcaJBi/pqqJBIQ8U9Mcpc9i8Aqxn88Skv5d+xBX006BY5u8N3mGLHa5Lgppa7L/HfwgwLgZ6NYs+Ag6uUmJRA==} + dev: false + + /brace-expansion@1.1.11: + resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + + /brace-expansion@2.0.1: + resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} + dependencies: + balanced-match: 1.0.2 + dev: false + + /braces@3.0.2: + resolution: {integrity: sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==} + engines: {node: '>=8'} + dependencies: + fill-range: 7.0.1 + + /browserslist@4.22.2: + resolution: {integrity: sha512-0UgcrvQmBDvZHFGdYUehrCNIazki7/lUP3kkoi/r3YB2amZbFM9J43ZRkJTXBUZK4gmx56+Sqk9+Vs9mwZx9+A==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + dependencies: + caniuse-lite: 1.0.30001571 + electron-to-chromium: 1.4.616 + node-releases: 2.0.14 + update-browserslist-db: 1.0.13(browserslist@4.22.2) + + /bs-logger@0.2.6: + resolution: {integrity: sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==} + engines: {node: '>= 6'} + dependencies: + fast-json-stable-stringify: 2.1.0 + + /bser@2.1.1: + resolution: {integrity: sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==} + dependencies: + node-int64: 0.4.0 + + /buffer-crc32@0.2.13: + resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==} + dev: false + + /buffer-from@1.1.2: + resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + + /buffer@5.7.1: + resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + dev: false + + /builtin-modules@3.3.0: + resolution: {integrity: sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==} + engines: {node: '>=6'} + dev: true + + /bundle-require@4.0.2(esbuild@0.19.11): + resolution: {integrity: sha512-jwzPOChofl67PSTW2SGubV9HBQAhhR2i6nskiOThauo9dzwDUgOWQScFVaJkjEfYX+UXiD+LEx8EblQMc2wIag==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + peerDependencies: + esbuild: '>=0.17' + dependencies: + esbuild: 0.19.11 + load-tsconfig: 0.2.5 + dev: false + + /bytes@3.1.2: + resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} + engines: {node: '>= 0.8'} + dev: false + + /cac@6.7.14: + resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} + engines: {node: '>=8'} + dev: false + + /cacheable-lookup@5.0.4: + resolution: {integrity: sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==} + engines: {node: '>=10.6.0'} + dev: false + + /cacheable-request@7.0.4: + resolution: {integrity: sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg==} + engines: {node: '>=8'} + dependencies: + clone-response: 1.0.3 + get-stream: 5.2.0 + http-cache-semantics: 4.1.1 + keyv: 4.5.4 + lowercase-keys: 2.0.0 + normalize-url: 6.1.0 + responselike: 2.0.1 + dev: false + + /call-bind@1.0.5: + resolution: {integrity: sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==} + dependencies: + function-bind: 1.1.2 + get-intrinsic: 1.2.2 + set-function-length: 1.1.1 + + /callsites@3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} + + /camelcase-keys@6.2.2: + resolution: {integrity: sha512-YrwaA0vEKazPBkn0ipTiMpSajYDSe+KjQfrjhcBMxJt/znbvlHd8Pw/Vamaz5EB4Wfhs3SUR3Z9mwRu/P3s3Yg==} + engines: {node: '>=8'} + dependencies: + camelcase: 5.3.1 + map-obj: 4.3.0 + quick-lru: 4.0.1 + dev: false + + /camelcase@5.3.1: + resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==} + engines: {node: '>=6'} + + /camelcase@6.3.0: + resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==} + engines: {node: '>=10'} + + /caniuse-lite@1.0.30001571: + resolution: {integrity: sha512-tYq/6MoXhdezDLFZuCO/TKboTzuQ/xR5cFdgXPfDtM7/kchBO3b4VWghE/OAi/DV7tTdhmLjZiZBZi1fA/GheQ==} + + /cardinal@2.1.1: + resolution: {integrity: sha512-JSr5eOgoEymtYHBjNWyjrMqet9Am2miJhlfKNdqLp6zoeAh0KN5dRAcxlecj5mAJrmQomgiOBj35xHLrFjqBpw==} + hasBin: true + dependencies: + ansicolors: 0.3.2 + redeyed: 2.1.1 + dev: false + + /case@1.6.3: + resolution: {integrity: sha512-mzDSXIPaFwVDvZAHqZ9VlbyF4yyXRuX6IvB06WvPYkqJVO24kX1PPhv9bfpKNFZyxYFmmgo03HUiD8iklmJYRQ==} + engines: {node: '>= 0.8.0'} + dev: false + + /cdktf-cli@0.20.0-pre.70(ink@3.2.0)(react@17.0.2): + resolution: {integrity: sha512-fH215jUamX8djZGQeuECldkZqsfCcpmmfhL6Zuaf6tKIzGeWWISGPvqO8g4zIrYl7uVcLHDz80U9+DDXNxsmQQ==} + hasBin: true + dependencies: + '@cdktf/cli-core': 0.20.0-pre.70(react@17.0.2) + '@cdktf/commons': 0.20.0-pre.70(constructs@10.3.0) + '@cdktf/hcl2cdk': 0.20.0-pre.70(constructs@10.3.0) + '@cdktf/hcl2json': 0.20.0-pre.70 + '@inquirer/prompts': 2.3.1 + '@sentry/node': 7.92.0 + cdktf: 0.20.0-pre.70(constructs@10.3.0) + ci-info: 3.9.0 + codemaker: 1.94.0 + constructs: 10.3.0 + cross-spawn: 7.0.3 + https-proxy-agent: 5.0.1 + ink-select-input: 4.2.2(ink@3.2.0)(react@17.0.2) + ink-table: 3.1.0(ink@3.2.0)(react@17.0.2) + jsii: 5.3.3 + jsii-pacmak: 1.94.0 + minimatch: 5.1.6 + node-fetch: 2.7.0 + pidtree: 0.6.0 + pidusage: 3.0.2 + tunnel-agent: 0.6.0 + xml-js: 1.6.11 + yargs: 17.7.2 + yoga-layout-prebuilt: 1.10.0 + zod: 3.22.4 + transitivePeerDependencies: + - '@types/react' + - bufferutil + - debug + - encoding + - ink + - react + - supports-color + - utf-8-validate + dev: false + + /cdktf@0.20.0-pre.70(constructs@10.3.0): + resolution: {integrity: sha512-oWIkBwdIaOT9fipTUwf2+hth0zC61SAOOV9+8jdcU0rwZGWuU22d8c7YJW2rN9e5t+/x07k9uy45oktQdhoAiw==} + peerDependencies: + constructs: ^10.0.25 + dependencies: + archiver: 5.3.2 + constructs: 10.3.0 + json-stable-stringify: 1.1.0 + semver: 7.5.4 + dev: false + bundledDependencies: + - archiver + - json-stable-stringify + - semver + + /chai-as-promised@7.1.1(chai@4.3.7): + resolution: {integrity: sha512-azL6xMoi+uxu6z4rhWQ1jbdUhOMhis2PvscD/xjLqNMkv3BPPp2JyyuTHOrf9BOosGpNQ11v6BKv/g57RXbiaA==} + peerDependencies: + chai: '>= 2.1.2 < 5' + dependencies: + chai: 4.3.7 + check-error: 1.0.3 + dev: true + + /chai-spies@1.0.0(chai@4.3.7): + resolution: {integrity: sha512-elF2ZUczBsFoP07qCfMO/zeggs8pqCf3fZGyK5+2X4AndS8jycZYID91ztD9oQ7d/0tnS963dPkd0frQEThDsg==} + engines: {node: '>= 4.0.0'} + peerDependencies: + chai: '*' + dependencies: + chai: 4.3.7 + dev: true + + /chai@4.3.7: + resolution: {integrity: sha512-HLnAzZ2iupm25PlN0xFreAlBA5zaBSv3og0DdeGA4Ar6h6rJ3A0rolRUKJhSF2V10GZKDgWF/VmAEsNWjCRB+A==} + engines: {node: '>=4'} + dependencies: + assertion-error: 1.1.0 + check-error: 1.0.3 + deep-eql: 4.1.3 + get-func-name: 2.0.2 + loupe: 2.3.7 + pathval: 1.1.1 + type-detect: 4.0.8 + dev: true + + /chalk@2.4.2: + resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==} + engines: {node: '>=4'} + dependencies: + ansi-styles: 3.2.1 + escape-string-regexp: 1.0.5 + supports-color: 5.5.0 + + /chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + /chalk@5.3.0: + resolution: {integrity: sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==} + engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} + dev: false + + /char-regex@1.0.2: + resolution: {integrity: sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==} + engines: {node: '>=10'} + + /chardet@0.7.0: + resolution: {integrity: sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==} + dev: false + + /charenc@0.0.2: + resolution: {integrity: sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA==} + dev: false + + /check-error@1.0.3: + resolution: {integrity: sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==} + dependencies: + get-func-name: 2.0.2 + dev: true + + /chokidar@3.5.3: + resolution: {integrity: sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==} + engines: {node: '>= 8.10.0'} + dependencies: + anymatch: 3.1.3 + braces: 3.0.2 + glob-parent: 5.1.2 + is-binary-path: 2.1.0 + is-glob: 4.0.3 + normalize-path: 3.0.0 + readdirp: 3.6.0 + optionalDependencies: + fsevents: 2.3.3 + + /chownr@1.1.4: + resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==} + dev: false + + /ci-info@2.0.0: + resolution: {integrity: sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==} + dev: false + + /ci-info@3.9.0: + resolution: {integrity: sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==} + engines: {node: '>=8'} + + /cjs-module-lexer@1.2.3: + resolution: {integrity: sha512-0TNiGstbQmCFwt4akjjBg5pLRTSyj/PkWQ1ZoO2zntmg9yLqSRxwEa4iCfQLGjqhiqBfOJa7W/E8wfGrTDmlZQ==} + + /clean-regexp@1.0.0: + resolution: {integrity: sha512-GfisEZEJvzKrmGWkvfhgzcz/BllN1USeqD2V6tg14OAOgaCD2Z/PUEuxnAZ/nPvmaHRG7a8y77p1T/IRQ4D1Hw==} + engines: {node: '>=4'} + dependencies: + escape-string-regexp: 1.0.5 + dev: true + + /clean-stack@2.2.0: + resolution: {integrity: sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==} + engines: {node: '>=6'} + dev: false + + /clean-stack@5.2.0: + resolution: {integrity: sha512-TyUIUJgdFnCISzG5zu3291TAsE77ddchd0bepon1VVQrKLGKFED4iXFEDQ24mIPdPBbyE16PK3F8MYE1CmcBEQ==} + engines: {node: '>=14.16'} + dependencies: + escape-string-regexp: 5.0.0 + dev: false + + /cli-boxes@2.2.1: + resolution: {integrity: sha512-y4coMcylgSCdVinjiDBuR8PCC2bLjyGTwEmPb9NHR/QaNU6EUOXcTY/s6VjGMD6ENSEaeQYHCY0GNGS5jfMwPw==} + engines: {node: '>=6'} + dev: false + + /cli-cursor@3.1.0: + resolution: {integrity: sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==} + engines: {node: '>=8'} + dependencies: + restore-cursor: 3.1.0 + dev: false + + /cli-spinners@2.9.1: + resolution: {integrity: sha512-jHgecW0pxkonBJdrKsqxgRX9AcG+u/5k0Q7WPDfi8AogLAdwxEkyYYNWwZ5GvVFoFx2uiY1eNcSK00fh+1+FyQ==} + engines: {node: '>=6'} + dev: false + + /cli-spinners@2.9.2: + resolution: {integrity: sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==} + engines: {node: '>=6'} + dev: false + + /cli-table3@0.6.3: + resolution: {integrity: sha512-w5Jac5SykAeZJKntOxJCrm63Eg5/4dhMWIcuTbo9rpE+brgaSZo0RuNJZeOyMgsUdhDeojvgyQLmjI+K50ZGyg==} + engines: {node: 10.* || >= 12.*} + dependencies: + string-width: 4.2.3 + optionalDependencies: + '@colors/colors': 1.5.0 + dev: false + + /cli-truncate@2.1.0: + resolution: {integrity: sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==} + engines: {node: '>=8'} + dependencies: + slice-ansi: 3.0.0 + string-width: 4.2.3 + dev: false + + /cli-width@4.1.0: + resolution: {integrity: sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==} + engines: {node: '>= 12'} + dev: false + + /cliui@6.0.0: + resolution: {integrity: sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==} + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 6.2.0 + dev: false + + /cliui@7.0.4: + resolution: {integrity: sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==} + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + dev: false + + /cliui@8.0.1: + resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} + engines: {node: '>=12'} + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + + /clone-response@1.0.3: + resolution: {integrity: sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==} + dependencies: + mimic-response: 1.0.1 + dev: false + + /clone@2.1.2: + resolution: {integrity: sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==} + engines: {node: '>=0.8'} + dev: false + + /cluster-key-slot@1.1.2: + resolution: {integrity: sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==} + engines: {node: '>=0.10.0'} + dev: false + + /co@4.6.0: + resolution: {integrity: sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==} + engines: {iojs: '>= 1.0.0', node: '>= 0.12.0'} + + /code-excerpt@3.0.0: + resolution: {integrity: sha512-VHNTVhd7KsLGOqfX3SyeO8RyYPMp1GJOg194VITk04WMYCv4plV68YWe6TJZxd9MhobjtpMRnVky01gqZsalaw==} + engines: {node: '>=10'} + dependencies: + convert-to-spaces: 1.0.2 + dev: false + + /codemaker@1.94.0: + resolution: {integrity: sha512-V+896C7RojQVfG0UlOXaFfVVxmFb08rPtJvzcxhdJfowc2o6xGwGG0OpWSLHy6fQrmt4BxLXnKZ6Xeuqt4aKjw==} + engines: {node: '>= 14.17.0'} + dependencies: + camelcase: 6.3.0 + decamelize: 5.0.1 + fs-extra: 10.1.0 + dev: false + + /collect-v8-coverage@1.0.2: + resolution: {integrity: sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==} + + /color-convert@1.9.3: + resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} + dependencies: + color-name: 1.1.3 + + /color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + dependencies: + color-name: 1.1.4 + + /color-name@1.1.3: + resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==} + + /color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + /color-string@1.9.1: + resolution: {integrity: sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==} + dependencies: + color-name: 1.1.4 + simple-swizzle: 0.2.2 + dev: false + + /color@3.2.1: + resolution: {integrity: sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==} + dependencies: + color-convert: 1.9.3 + color-string: 1.9.1 + dev: false + + /colorspace@1.1.4: + resolution: {integrity: sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w==} + dependencies: + color: 3.2.1 + text-hex: 1.0.0 + dev: false + + /combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + dependencies: + delayed-stream: 1.0.0 + dev: false + + /commander@4.1.1: + resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} + engines: {node: '>= 6'} + dev: false + + /commonmark@0.30.0: + resolution: {integrity: sha512-j1yoUo4gxPND1JWV9xj5ELih0yMv1iCWDG6eEQIPLSWLxzCXiFoyS7kvB+WwU+tZMf4snwJMMtaubV0laFpiBA==} + hasBin: true + dependencies: + entities: 2.0.3 + mdurl: 1.0.1 + minimist: 1.2.8 + string.prototype.repeat: 0.2.0 + dev: false + + /compare-func@2.0.0: + resolution: {integrity: sha512-zHig5N+tPWARooBnb0Zx1MFcdfpyJrfTJ3Y5L+IFvUm8rM74hHz66z0gw0x4tijh5CorKkKUCnW82R2vmpeCRA==} + dependencies: + array-ify: 1.0.0 + dot-prop: 5.3.0 + dev: false + + /component-emitter@1.3.1: + resolution: {integrity: sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==} + dev: false + + /compress-commons@4.1.2: + resolution: {integrity: sha512-D3uMHtGc/fcO1Gt1/L7i1e33VOvD4A9hfQLP+6ewd+BvG/gQ84Yh4oftEhAdjSMgBgwGL+jsppT7JYNpo6MHHg==} + engines: {node: '>= 10'} + dependencies: + buffer-crc32: 0.2.13 + crc32-stream: 4.0.3 + normalize-path: 3.0.0 + readable-stream: 3.6.2 + dev: false + + /concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + + /config-chain@1.1.13: + resolution: {integrity: sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==} + dependencies: + ini: 1.3.8 + proto-list: 1.2.4 + dev: false + + /constructs@10.3.0: + resolution: {integrity: sha512-vbK8i3rIb/xwZxSpTjz3SagHn1qq9BChLEfy5Hf6fB3/2eFbrwt2n9kHwQcS0CPTRBesreeAcsJfMq2229FnbQ==} + engines: {node: '>= 16.14.0'} + dev: false + + /content-disposition@0.5.4: + resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==} + engines: {node: '>= 0.6'} + dependencies: + safe-buffer: 5.2.1 + dev: false + + /content-type@1.0.5: + resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} + engines: {node: '>= 0.6'} + dev: false + + /conventional-changelog-angular@6.0.0: + resolution: {integrity: sha512-6qLgrBF4gueoC7AFVHu51nHL9pF9FRjXrH+ceVf7WmAfH3gs+gEYOkvxhjMPjZu57I4AGUGoNTY8V7Hrgf1uqg==} + engines: {node: '>=14'} + dependencies: + compare-func: 2.0.0 + dev: false + + /conventional-changelog-writer@6.0.1: + resolution: {integrity: sha512-359t9aHorPw+U+nHzUXHS5ZnPBOizRxfQsWT5ZDHBfvfxQOAik+yfuhKXG66CN5LEWPpMNnIMHUTCKeYNprvHQ==} + engines: {node: '>=14'} + hasBin: true + dependencies: + conventional-commits-filter: 3.0.0 + dateformat: 3.0.3 + handlebars: 4.7.8 + json-stringify-safe: 5.0.1 + meow: 8.1.2 + semver: 7.5.4 + split: 1.0.1 + dev: false + + /conventional-commits-filter@3.0.0: + resolution: {integrity: sha512-1ymej8b5LouPx9Ox0Dw/qAO2dVdfpRFq28e5Y0jJEU8ZrLdy0vOSkkIInwmxErFGhg6SALro60ZrwYFVTUDo4Q==} + engines: {node: '>=14'} + dependencies: + lodash.ismatch: 4.4.0 + modify-values: 1.0.1 + dev: false + + /conventional-commits-filter@4.0.0: + resolution: {integrity: sha512-rnpnibcSOdFcdclpFwWa+pPlZJhXE7l+XK04zxhbWrhgpR96h33QLz8hITTXbcYICxVr3HZFtbtUAQ+4LdBo9A==} + engines: {node: '>=16'} + dev: false + + /conventional-commits-parser@5.0.0: + resolution: {integrity: sha512-ZPMl0ZJbw74iS9LuX9YIAiW8pfM5p3yh2o/NbXHbkFuZzY5jvdi5jFycEOkmBW5H5I7nA+D6f3UcsCLP2vvSEA==} + engines: {node: '>=16'} + hasBin: true + dependencies: + JSONStream: 1.3.5 + is-text-path: 2.0.0 + meow: 12.1.1 + split2: 4.2.0 + dev: false + + /convert-source-map@1.9.0: + resolution: {integrity: sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==} + dev: true + + /convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + + /convert-to-spaces@1.0.2: + resolution: {integrity: sha512-cj09EBuObp9gZNQCzc7hByQyrs6jVGE+o9kSJmeUoj+GiPiJvi5LYqEH/Hmme4+MTLHM+Ejtq+FChpjjEnsPdQ==} + engines: {node: '>= 4'} + dev: false + + /cookie-signature@1.0.6: + resolution: {integrity: sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==} + dev: false + + /cookie@0.4.2: + resolution: {integrity: sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==} + engines: {node: '>= 0.6'} + dev: false + + /cookie@0.5.0: + resolution: {integrity: sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==} + engines: {node: '>= 0.6'} + dev: false + + /cookiejar@2.1.4: + resolution: {integrity: sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==} + dev: false + + /core-util-is@1.0.3: + resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} + dev: false + + /cors@2.8.5: + resolution: {integrity: sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==} + engines: {node: '>= 0.10'} + dependencies: + object-assign: 4.1.1 + vary: 1.1.2 + dev: false + + /cosmiconfig@8.3.6(typescript@5.3.3): + resolution: {integrity: sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==} + engines: {node: '>=14'} + peerDependencies: + typescript: '>=4.9.5' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + import-fresh: 3.3.0 + js-yaml: 4.1.0 + parse-json: 5.2.0 + path-type: 4.0.0 + typescript: 5.3.3 + dev: false + + /crc-32@1.2.2: + resolution: {integrity: sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==} + engines: {node: '>=0.8'} + hasBin: true + dev: false + + /crc32-stream@4.0.3: + resolution: {integrity: sha512-NT7w2JVU7DFroFdYkeq8cywxrgjPHWkdX1wjpRQXPX5Asews3tA+Ght6lddQO5Mkumffp3X7GEqku3epj2toIw==} + engines: {node: '>= 10'} + dependencies: + crc-32: 1.2.2 + readable-stream: 3.6.2 + dev: false + + /create-jest@29.7.0(@types/node@20.10.8)(ts-node@10.9.2): + resolution: {integrity: sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + hasBin: true + dependencies: + '@jest/types': 29.6.3 + chalk: 4.1.2 + exit: 0.1.2 + graceful-fs: 4.2.11 + jest-config: 29.7.0(@types/node@20.10.8)(ts-node@10.9.2) + jest-util: 29.7.0 + prompts: 2.4.2 + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - supports-color + - ts-node + + /create-require@1.1.1: + resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==} + + /cross-fetch@3.1.5: + resolution: {integrity: sha512-lvb1SBsI0Z7GDwmuid+mU3kWVBwTVUbe7S0H52yaaAdQOXq2YktTCZdlAcNKFzE6QtRz0snpw9bNiPeOIkkQvw==} + dependencies: + node-fetch: 2.6.7 + transitivePeerDependencies: + - encoding + dev: false + + /cross-fetch@3.1.8: + resolution: {integrity: sha512-cvA+JwZoU0Xq+h6WkMvAUqPEYy92Obet6UdKLfW60qn99ftItKjB5T+BkyWOFWe2pUyfQ+IJHmpOTznqk1M6Kg==} + dependencies: + node-fetch: 2.7.0 + transitivePeerDependencies: + - encoding + dev: false + + /cross-spawn@7.0.3: + resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} + engines: {node: '>= 8'} + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + /crypt@0.0.2: + resolution: {integrity: sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow==} + dev: false + + /crypto-random-string@4.0.0: + resolution: {integrity: sha512-x8dy3RnvYdlUcPOjkEHqozhiwzKNSq7GcPuXFbnyMOCHxX8V3OgIg/pYuabl2sbUPfIJaeAQB7PMOK8DFIdoRA==} + engines: {node: '>=12'} + dependencies: + type-fest: 1.4.0 + dev: false + + /damerau-levenshtein@1.0.8: + resolution: {integrity: sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==} + dev: true + + /dataloader@2.2.2: + resolution: {integrity: sha512-8YnDaaf7N3k/q5HnTJVuzSyLETjoZjVmHc4AeKAzOvKHEFQKcn64OKBfzHYtE9zGjctNM7V9I0MfnUVLpi7M5g==} + dev: false + + /date-format@4.0.14: + resolution: {integrity: sha512-39BOQLs9ZjKh0/patS9nrT8wc3ioX3/eA/zgbKNopnF2wCqJEoxywwwElATYvRsXdnOxA/OQeQoFZ3rFjVajhg==} + engines: {node: '>=4.0'} + dev: false + + /dateformat@3.0.3: + resolution: {integrity: sha512-jyCETtSl3VMZMWeRo7iY1FL19ges1t55hMo5yaam4Jrsm5EPL89UQkoQRyiI+Yf4k8r2ZpdngkV8hr1lIdjb3Q==} + dev: false + + /debug@2.6.9: + resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + dependencies: + ms: 2.0.0 + dev: false + + /debug@3.2.7(supports-color@5.5.0): + resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + dependencies: + ms: 2.1.3 + supports-color: 5.5.0 + dev: true + + /debug@4.3.4: + resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + dependencies: + ms: 2.1.2 + + /decamelize-keys@1.1.1: + resolution: {integrity: sha512-WiPxgEirIV0/eIOMcnFBA3/IJZAZqKnwAwWyvvdi4lsr1WCN22nhdf/3db3DoZcUjTV2SqfzIwNyp6y2xs3nmg==} + engines: {node: '>=0.10.0'} + dependencies: + decamelize: 1.2.0 + map-obj: 1.0.1 + dev: false + + /decamelize@1.2.0: + resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==} + engines: {node: '>=0.10.0'} + dev: false + + /decamelize@5.0.1: + resolution: {integrity: sha512-VfxadyCECXgQlkoEAjeghAr5gY3Hf+IKjKb+X8tGVDtveCjN+USwprd2q3QXBR9T1+x2DG0XZF5/w+7HAtSaXA==} + engines: {node: '>=10'} + dev: false + + /decompress-response@6.0.0: + resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==} + engines: {node: '>=10'} + dependencies: + mimic-response: 3.1.0 + dev: false + + /dedent@0.7.0: + resolution: {integrity: sha512-Q6fKUPqnAHAyhiUgFU7BUzLiv0kd8saH9al7tnu5Q/okj6dnupxyTgFIBjVzJATdfIAm9NAsvXNzjaKa+bxVyA==} + dev: true + + /dedent@1.5.1: + resolution: {integrity: sha512-+LxW+KLWxu3HW3M2w2ympwtqPrqYRzU8fqi6Fhd18fBALe15blJPI/I4+UHveMVG6lJqB4JNd4UG0S5cnVHwIg==} + peerDependencies: + babel-plugin-macros: ^3.1.0 + peerDependenciesMeta: + babel-plugin-macros: + optional: true + + /deep-eql@4.1.3: + resolution: {integrity: sha512-WaEtAOpRA1MQ0eohqZjpGD8zdI0Ovsm8mmFhaDN8dvDZzyoUMcYDnf5Y6iu7HTXxf8JDS23qWa4a+hKCDyOPzw==} + engines: {node: '>=6'} + dependencies: + type-detect: 4.0.8 + dev: true + + /deep-equal-in-any-order@1.1.20: + resolution: {integrity: sha512-GTpQxcQx28KvV6ChrHb4AcL5z+eFEymtoSUaWe+FEakEpZ/66jTvz52xLUym/sK4Pn1sbYLsCV5rEURDeiFl3w==} + dependencies: + lodash.mapvalues: 4.6.0 + sort-any: 2.0.0 + dev: false + + /deep-equal@2.2.3: + resolution: {integrity: sha512-ZIwpnevOurS8bpT4192sqAowWM76JDKSHYzMLty3BZGSswgq6pBaH3DhCSW5xVAZICZyKdOBPjwww5wfgT/6PA==} + engines: {node: '>= 0.4'} + dependencies: + array-buffer-byte-length: 1.0.0 + call-bind: 1.0.5 + es-get-iterator: 1.1.3 + get-intrinsic: 1.2.2 + is-arguments: 1.1.1 + is-array-buffer: 3.0.2 + is-date-object: 1.0.5 + is-regex: 1.1.4 + is-shared-array-buffer: 1.0.2 + isarray: 2.0.5 + object-is: 1.1.5 + object-keys: 1.1.1 + object.assign: 4.1.5 + regexp.prototype.flags: 1.5.1 + side-channel: 1.0.4 + which-boxed-primitive: 1.0.2 + which-collection: 1.0.1 + which-typed-array: 1.1.13 + dev: false + + /deep-extend@0.6.0: + resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==} + engines: {node: '>=4.0.0'} + dev: false + + /deep-is@0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + + /deepmerge@4.3.1: + resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} + engines: {node: '>=0.10.0'} + + /defer-to-connect@2.0.1: + resolution: {integrity: sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==} + engines: {node: '>=10'} + dev: false + + /define-data-property@1.1.1: + resolution: {integrity: sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==} + engines: {node: '>= 0.4'} + dependencies: + get-intrinsic: 1.2.2 + gopd: 1.0.1 + has-property-descriptors: 1.0.1 + + /define-properties@1.2.1: + resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} + engines: {node: '>= 0.4'} + dependencies: + define-data-property: 1.1.1 + has-property-descriptors: 1.0.1 + object-keys: 1.1.1 + + /delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + dev: false + + /denque@1.5.1: + resolution: {integrity: sha512-XwE+iZ4D6ZUB7mfYRMb5wByE8L74HCn30FBN7sWnXksWc1LO1bPDl67pBR9o/kC4z/xSNAwkMYcGgqDV3BE3Hw==} + engines: {node: '>=0.10'} + dev: false + + /depd@2.0.0: + resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} + engines: {node: '>= 0.8'} + dev: false + + /deprecation@2.3.1: + resolution: {integrity: sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==} + dev: false + + /dequal@2.0.3: + resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} + engines: {node: '>=6'} + dev: true + + /destroy@1.2.0: + resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==} + engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + dev: false + + /detect-indent@5.0.0: + resolution: {integrity: sha512-rlpvsxUtM0PQvy9iZe640/IWwWYyBsTApREbA1pHOpmOUIl9MkP/U4z7vTtg4Oaojvqhxt7sdufnT0EzGaR31g==} + engines: {node: '>=4'} + dev: false + + /detect-indent@7.0.1: + resolution: {integrity: sha512-Mc7QhQ8s+cLrnUfU/Ji94vG/r8M26m8f++vyres4ZoojaRDpZ1eSIh/EpzLNwlWuvzSZ3UbDFspjFvTDXe6e/g==} + engines: {node: '>=12.20'} + dev: true + + /detect-libc@2.0.2: + resolution: {integrity: sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw==} + engines: {node: '>=8'} + dev: false + + /detect-newline@2.1.0: + resolution: {integrity: sha512-CwffZFvlJffUg9zZA0uqrjQayUTC8ob94pnr5sFwaVv3IOmkfUHcWH+jXaQK3askE51Cqe8/9Ql/0uXNwqZ8Zg==} + engines: {node: '>=0.10.0'} + dev: false + + /detect-newline@3.1.0: + resolution: {integrity: sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==} + engines: {node: '>=8'} + + /detect-newline@4.0.1: + resolution: {integrity: sha512-qE3Veg1YXzGHQhlA6jzebZN2qVf6NX+A7m7qlhCGG30dJixrAQhYOsJjsnBjJkCSmuOPpCk30145fr8FV0bzog==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dev: true + + /detect-port@1.5.1: + resolution: {integrity: sha512-aBzdj76lueB6uUst5iAs7+0H/oOjqI5D16XUWxlWMIMROhcM0rfsNVk93zTngq1dDNpoXRr++Sus7ETAExppAQ==} + hasBin: true + dependencies: + address: 1.2.2 + debug: 4.3.4 + transitivePeerDependencies: + - supports-color + dev: false + + /dezalgo@1.0.4: + resolution: {integrity: sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==} + dependencies: + asap: 2.0.6 + wrappy: 1.0.2 + dev: false + + /diff-sequences@28.1.1: + resolution: {integrity: sha512-FU0iFaH/E23a+a718l8Qa/19bF9p06kgE0KipMOMadwa3SjnaElKzPaUC0vnibs6/B/9ni97s61mcejk8W1fQw==} + engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} + dev: true + + /diff-sequences@29.6.3: + resolution: {integrity: sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + /diff@4.0.2: + resolution: {integrity: sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==} + engines: {node: '>=0.3.1'} + + /diff@5.1.0: + resolution: {integrity: sha512-D+mk+qE8VC/PAUrlAU34N+VfXev0ghe5ywmpqrawphmVZc1bEfn56uo9qpyGp1p4xpzOHkSW4ztBd6L7Xx4ACw==} + engines: {node: '>=0.3.1'} + dev: true + + /dir-glob@3.0.1: + resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} + engines: {node: '>=8'} + dependencies: + path-type: 4.0.0 + + /doctrine@2.1.0: + resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==} + engines: {node: '>=0.10.0'} + dependencies: + esutils: 2.0.3 + dev: true + + /doctrine@3.0.0: + resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==} + engines: {node: '>=6.0.0'} + dependencies: + esutils: 2.0.3 + + /dot-prop@5.3.0: + resolution: {integrity: sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==} + engines: {node: '>=8'} + dependencies: + is-obj: 2.0.0 + dev: false + + /dotenv-cli@7.3.0: + resolution: {integrity: sha512-314CA4TyK34YEJ6ntBf80eUY+t1XaFLyem1k9P0sX1gn30qThZ5qZr/ZwE318gEnzyYP9yj9HJk6SqwE0upkfw==} + hasBin: true + dependencies: + cross-spawn: 7.0.3 + dotenv: 16.3.1 + dotenv-expand: 10.0.0 + minimist: 1.2.8 + dev: false + + /dotenv-expand@10.0.0: + resolution: {integrity: sha512-GopVGCpVS1UKH75VKHGuQFqS1Gusej0z4FyQkPdwjil2gNIv+LNsqBlboOzpJFZKVT95GkCyWJbBSdFEFUWI2A==} + engines: {node: '>=12'} + dev: false + + /dotenv@16.0.3: + resolution: {integrity: sha512-7GO6HghkA5fYG9TYnNxi14/7K9f5occMlp3zXAuSxn7CKCxt9xbNWG7yF8hTCSUchlfWSe3uLmlPfigevRItzQ==} + engines: {node: '>=12'} + dev: true + + /dotenv@16.3.1: + resolution: {integrity: sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ==} + engines: {node: '>=12'} + dev: false + + /downlevel-dts@0.11.0: + resolution: {integrity: sha512-vo835pntK7kzYStk7xUHDifiYJvXxVhUapt85uk2AI94gUUAQX9HNRtrcMHNSc3YHJUEHGbYIGsM99uIbgAtxw==} + hasBin: true + dependencies: + semver: 7.5.4 + shelljs: 0.8.5 + typescript: 5.4.0-dev.20240110 + dev: false + + /duplexer2@0.1.4: + resolution: {integrity: sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA==} + dependencies: + readable-stream: 2.3.8 + dev: false + + /eastasianwidth@0.2.0: + resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + dev: false + + /ee-first@1.1.1: + resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} + dev: false + + /electron-to-chromium@1.4.616: + resolution: {integrity: sha512-1n7zWYh8eS0L9Uy+GskE0lkBUNK83cXTVJI0pU3mGprFsbfSdAc15VTFbo+A+Bq4pwstmL30AVcEU3Fo463lNg==} + + /emittery@0.10.2: + resolution: {integrity: sha512-aITqOwnLanpHLNXZJENbOgjUBeHocD+xsSJmNrjovKBW5HbSpW3d1pEls7GFQPUWXiwG9+0P4GtHfEqC/4M0Iw==} + engines: {node: '>=12'} + dev: true + + /emittery@0.13.1: + resolution: {integrity: sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==} + engines: {node: '>=12'} + + /emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + /emoji-regex@9.2.2: + resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + + /enabled@2.0.0: + resolution: {integrity: sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==} + dev: false + + /encodeurl@1.0.2: + resolution: {integrity: sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==} + engines: {node: '>= 0.8'} + dev: false + + /end-of-stream@1.4.4: + resolution: {integrity: sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==} + dependencies: + once: 1.4.0 + dev: false + + /enhanced-resolve@5.15.0: + resolution: {integrity: sha512-LXYT42KJ7lpIKECr2mAXIaMldcNCh/7E0KBKOu4KSfkHmP+mZmSs+8V5gBAqisWBy0OO4W5Oyys0GO1Y8KtdKg==} + engines: {node: '>=10.13.0'} + dependencies: + graceful-fs: 4.2.11 + tapable: 2.2.1 + dev: true + + /entities@2.0.3: + resolution: {integrity: sha512-MyoZ0jgnLvB2X3Lg5HqpFmn1kybDiIfEQmKzTb5apr51Rb+T3KdmMiqa70T+bhGnyv7bQ6WMj2QMHpGMmlrUYQ==} + dev: false + + /env-ci@9.1.1: + resolution: {integrity: sha512-Im2yEWeF4b2RAMAaWvGioXk6m0UNaIjD8hj28j2ij5ldnIFrDQT0+pzDvpbRkcjurhXhf/AsBKv8P2rtmGi9Aw==} + engines: {node: ^16.14 || >=18} + dependencies: + execa: 7.2.0 + java-properties: 1.0.2 + dev: false + + /error-ex@1.3.2: + resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==} + dependencies: + is-arrayish: 0.2.1 + + /es-abstract@1.22.3: + resolution: {integrity: sha512-eiiY8HQeYfYH2Con2berK+To6GrK2RxbPawDkGq4UiCQQfZHb6wX9qQqkbpPqaxQFcl8d9QzZqo0tGE0VcrdwA==} + engines: {node: '>= 0.4'} + dependencies: + array-buffer-byte-length: 1.0.0 + arraybuffer.prototype.slice: 1.0.2 + available-typed-arrays: 1.0.5 + call-bind: 1.0.5 + es-set-tostringtag: 2.0.2 + es-to-primitive: 1.2.1 + function.prototype.name: 1.1.6 + get-intrinsic: 1.2.2 + get-symbol-description: 1.0.0 + globalthis: 1.0.3 + gopd: 1.0.1 + has-property-descriptors: 1.0.1 + has-proto: 1.0.1 + has-symbols: 1.0.3 + hasown: 2.0.0 + internal-slot: 1.0.6 + is-array-buffer: 3.0.2 + is-callable: 1.2.7 + is-negative-zero: 2.0.2 + is-regex: 1.1.4 + is-shared-array-buffer: 1.0.2 + is-string: 1.0.7 + is-typed-array: 1.1.12 + is-weakref: 1.0.2 + object-inspect: 1.13.1 + object-keys: 1.1.1 + object.assign: 4.1.5 + regexp.prototype.flags: 1.5.1 + safe-array-concat: 1.0.1 + safe-regex-test: 1.0.0 + string.prototype.trim: 1.2.8 + string.prototype.trimend: 1.0.7 + string.prototype.trimstart: 1.0.7 + typed-array-buffer: 1.0.0 + typed-array-byte-length: 1.0.0 + typed-array-byte-offset: 1.0.0 + typed-array-length: 1.0.4 + unbox-primitive: 1.0.2 + which-typed-array: 1.1.13 + dev: true + + /es-get-iterator@1.1.3: + resolution: {integrity: sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw==} + dependencies: + call-bind: 1.0.5 + get-intrinsic: 1.2.2 + has-symbols: 1.0.3 + is-arguments: 1.1.1 + is-map: 2.0.2 + is-set: 2.0.2 + is-string: 1.0.7 + isarray: 2.0.5 + stop-iteration-iterator: 1.0.0 + dev: false + + /es-iterator-helpers@1.0.15: + resolution: {integrity: sha512-GhoY8uYqd6iwUl2kgjTm4CZAf6oo5mHK7BPqx3rKgx893YSsy0LGHV6gfqqQvZt/8xM8xeOnfXBCfqclMKkJ5g==} + dependencies: + asynciterator.prototype: 1.0.0 + call-bind: 1.0.5 + define-properties: 1.2.1 + es-abstract: 1.22.3 + es-set-tostringtag: 2.0.2 + function-bind: 1.1.2 + get-intrinsic: 1.2.2 + globalthis: 1.0.3 + has-property-descriptors: 1.0.1 + has-proto: 1.0.1 + has-symbols: 1.0.3 + internal-slot: 1.0.6 + iterator.prototype: 1.1.2 + safe-array-concat: 1.0.1 + dev: true + + /es-set-tostringtag@2.0.2: + resolution: {integrity: sha512-BuDyupZt65P9D2D2vA/zqcI3G5xRsklm5N3xCwuiy+/vKy8i0ifdsQP1sLgO4tZDSCaQUSnmC48khknGMV3D2Q==} + engines: {node: '>= 0.4'} + dependencies: + get-intrinsic: 1.2.2 + has-tostringtag: 1.0.0 + hasown: 2.0.0 + dev: true + + /es-shim-unscopables@1.0.2: + resolution: {integrity: sha512-J3yBRXCzDu4ULnQwxyToo/OjdMx6akgVC7K6few0a7F/0wLtmKKN7I73AH5T2836UuXRqN7Qg+IIUw/+YJksRw==} + dependencies: + hasown: 2.0.0 + dev: true + + /es-to-primitive@1.2.1: + resolution: {integrity: sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==} + engines: {node: '>= 0.4'} + dependencies: + is-callable: 1.2.7 + is-date-object: 1.0.5 + is-symbol: 1.0.4 + dev: true + + /esbuild@0.19.11: + resolution: {integrity: sha512-HJ96Hev2hX/6i5cDVwcqiJBBtuo9+FeIJOtZ9W1kA5M6AMJRHUZlpYZ1/SbEwtO0ioNAW8rUooVpC/WehY2SfA==} + engines: {node: '>=12'} + hasBin: true + requiresBuild: true + optionalDependencies: + '@esbuild/aix-ppc64': 0.19.11 + '@esbuild/android-arm': 0.19.11 + '@esbuild/android-arm64': 0.19.11 + '@esbuild/android-x64': 0.19.11 + '@esbuild/darwin-arm64': 0.19.11 + '@esbuild/darwin-x64': 0.19.11 + '@esbuild/freebsd-arm64': 0.19.11 + '@esbuild/freebsd-x64': 0.19.11 + '@esbuild/linux-arm': 0.19.11 + '@esbuild/linux-arm64': 0.19.11 + '@esbuild/linux-ia32': 0.19.11 + '@esbuild/linux-loong64': 0.19.11 + '@esbuild/linux-mips64el': 0.19.11 + '@esbuild/linux-ppc64': 0.19.11 + '@esbuild/linux-riscv64': 0.19.11 + '@esbuild/linux-s390x': 0.19.11 + '@esbuild/linux-x64': 0.19.11 + '@esbuild/netbsd-x64': 0.19.11 + '@esbuild/openbsd-x64': 0.19.11 + '@esbuild/sunos-x64': 0.19.11 + '@esbuild/win32-arm64': 0.19.11 + '@esbuild/win32-ia32': 0.19.11 + '@esbuild/win32-x64': 0.19.11 + dev: false + + /escalade@3.1.1: + resolution: {integrity: sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==} + engines: {node: '>=6'} + + /escape-html@1.0.3: + resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + dev: false + + /escape-string-regexp@1.0.5: + resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} + engines: {node: '>=0.8.0'} + + /escape-string-regexp@2.0.0: + resolution: {integrity: sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==} + engines: {node: '>=8'} + + /escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + + /escape-string-regexp@5.0.0: + resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==} + engines: {node: '>=12'} + dev: false + + /eslint-config-prettier@8.8.0(eslint@8.45.0): + resolution: {integrity: sha512-wLbQiFre3tdGgpDv67NQKnJuTlcUVYHas3k+DZCc2U2BadthoEY4B7hLPvAxaqdyOGCzuLfii2fqGph10va7oA==} + hasBin: true + peerDependencies: + eslint: '>=7.0.0' + dependencies: + eslint: 8.45.0 + dev: false + + /eslint-config-prettier@8.8.0(eslint@8.56.0): + resolution: {integrity: sha512-wLbQiFre3tdGgpDv67NQKnJuTlcUVYHas3k+DZCc2U2BadthoEY4B7hLPvAxaqdyOGCzuLfii2fqGph10va7oA==} + hasBin: true + peerDependencies: + eslint: '>=7.0.0' + dependencies: + eslint: 8.56.0 + dev: true + + /eslint-config-turbo@1.11.2(eslint@8.56.0): + resolution: {integrity: sha512-vqbyCH6kCHFoIAWUmGL61c0BfUQNz0XAl2RzAnEkSQ+PLXvEvuV2HsvL51UOzyyElfJlzZuh9T4BvUqb5KR9Eg==} + peerDependencies: + eslint: '>6.6.0' + dependencies: + eslint: 8.56.0 + eslint-plugin-turbo: 1.11.2(eslint@8.56.0) + dev: true + + /eslint-import-resolver-alias@1.1.2(eslint-plugin-import@2.29.1): + resolution: {integrity: sha512-WdviM1Eu834zsfjHtcGHtGfcu+F30Od3V7I9Fi57uhBEwPkjDcii7/yW8jAT+gOhn4P/vOxxNAXbFAKsrrc15w==} + engines: {node: '>= 4'} + peerDependencies: + eslint-plugin-import: '>=1.4.0' + dependencies: + eslint-plugin-import: 2.29.1(@typescript-eslint/parser@6.1.0)(eslint@8.56.0) + dev: true + + /eslint-import-resolver-node@0.3.9: + resolution: {integrity: sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==} + dependencies: + debug: 3.2.7(supports-color@5.5.0) + is-core-module: 2.13.1 + resolve: 1.22.8 + transitivePeerDependencies: + - supports-color + dev: true + + /eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@5.62.0)(eslint-plugin-import@2.29.1)(eslint@8.56.0): + resolution: {integrity: sha512-xgdptdoi5W3niYeuQxKmzVDTATvLYqhpwmykwsh7f6HIOStGWEIL9iqZgQDF9u9OEzrRwR8no5q2VT+bjAujTg==} + engines: {node: ^14.18.0 || >=16.0.0} + peerDependencies: + eslint: '*' + eslint-plugin-import: '*' + dependencies: + debug: 4.3.4 + enhanced-resolve: 5.15.0 + eslint: 8.56.0 + eslint-module-utils: 2.8.0(@typescript-eslint/parser@5.62.0)(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@8.56.0) + eslint-plugin-import: 2.29.1(@typescript-eslint/parser@6.1.0)(eslint@8.56.0) + fast-glob: 3.3.2 + get-tsconfig: 4.7.2 + is-core-module: 2.13.1 + is-glob: 4.0.3 + transitivePeerDependencies: + - '@typescript-eslint/parser' + - eslint-import-resolver-node + - eslint-import-resolver-webpack + - supports-color + dev: true + + /eslint-module-utils@2.8.0(@typescript-eslint/parser@5.62.0)(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@8.56.0): + resolution: {integrity: sha512-aWajIYfsqCKRDgUfjEXNN/JlrzauMuSEy5sbd7WXbtW3EH6A6MpwEh42c7qD+MqQo9QMJ6fWLAeIJynx0g6OAw==} + engines: {node: '>=4'} + peerDependencies: + '@typescript-eslint/parser': '*' + eslint: '*' + eslint-import-resolver-node: '*' + eslint-import-resolver-typescript: '*' + eslint-import-resolver-webpack: '*' + peerDependenciesMeta: + '@typescript-eslint/parser': + optional: true + eslint: + optional: true + eslint-import-resolver-node: + optional: true + eslint-import-resolver-typescript: + optional: true + eslint-import-resolver-webpack: + optional: true + dependencies: + '@typescript-eslint/parser': 5.62.0(eslint@8.56.0)(typescript@5.3.3) + debug: 3.2.7(supports-color@5.5.0) + eslint: 8.56.0 + eslint-import-resolver-node: 0.3.9 + eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@5.62.0)(eslint-plugin-import@2.29.1)(eslint@8.56.0) + transitivePeerDependencies: + - supports-color + dev: true + + /eslint-module-utils@2.8.0(@typescript-eslint/parser@6.1.0)(eslint-import-resolver-node@0.3.9)(eslint@8.56.0): + resolution: {integrity: sha512-aWajIYfsqCKRDgUfjEXNN/JlrzauMuSEy5sbd7WXbtW3EH6A6MpwEh42c7qD+MqQo9QMJ6fWLAeIJynx0g6OAw==} + engines: {node: '>=4'} + peerDependencies: + '@typescript-eslint/parser': '*' + eslint: '*' + eslint-import-resolver-node: '*' + eslint-import-resolver-typescript: '*' + eslint-import-resolver-webpack: '*' + peerDependenciesMeta: + '@typescript-eslint/parser': + optional: true + eslint: + optional: true + eslint-import-resolver-node: + optional: true + eslint-import-resolver-typescript: + optional: true + eslint-import-resolver-webpack: + optional: true + dependencies: + '@typescript-eslint/parser': 6.1.0(eslint@8.56.0)(typescript@5.3.3) + debug: 3.2.7(supports-color@5.5.0) + eslint: 8.56.0 + eslint-import-resolver-node: 0.3.9 + transitivePeerDependencies: + - supports-color + dev: true + + /eslint-plugin-eslint-comments@3.2.0(eslint@8.56.0): + resolution: {integrity: sha512-0jkOl0hfojIHHmEHgmNdqv4fmh7300NdpA9FFpF7zaoLvB/QeXOGNLIo86oAveJFrfB1p05kC8hpEMHM8DwWVQ==} + engines: {node: '>=6.5.0'} + peerDependencies: + eslint: '>=4.19.1' + dependencies: + escape-string-regexp: 1.0.5 + eslint: 8.56.0 + ignore: 5.3.0 + dev: true + + /eslint-plugin-import@2.29.1(@typescript-eslint/parser@6.1.0)(eslint@8.56.0): + resolution: {integrity: sha512-BbPC0cuExzhiMo4Ff1BTVwHpjjv28C5R+btTOGaCRC7UEz801up0JadwkeSk5Ued6TG34uaczuVuH6qyy5YUxw==} + engines: {node: '>=4'} + peerDependencies: + '@typescript-eslint/parser': '*' + eslint: ^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 peerDependenciesMeta: - typescript: + '@typescript-eslint/parser': optional: true dependencies: - '@eslint-community/regexpp': 4.10.0 '@typescript-eslint/parser': 6.1.0(eslint@8.56.0)(typescript@5.3.3) - '@typescript-eslint/scope-manager': 6.1.0 - '@typescript-eslint/type-utils': 6.1.0(eslint@8.56.0)(typescript@5.3.3) - '@typescript-eslint/utils': 6.1.0(eslint@8.56.0)(typescript@5.3.3) - '@typescript-eslint/visitor-keys': 6.1.0 + array-includes: 3.1.7 + array.prototype.findlastindex: 1.2.3 + array.prototype.flat: 1.3.2 + array.prototype.flatmap: 1.3.2 + debug: 3.2.7(supports-color@5.5.0) + doctrine: 2.1.0 + eslint: 8.56.0 + eslint-import-resolver-node: 0.3.9 + eslint-module-utils: 2.8.0(@typescript-eslint/parser@6.1.0)(eslint-import-resolver-node@0.3.9)(eslint@8.56.0) + hasown: 2.0.0 + is-core-module: 2.13.1 + is-glob: 4.0.3 + minimatch: 3.1.2 + object.fromentries: 2.0.7 + object.groupby: 1.0.1 + object.values: 1.1.7 + semver: 6.3.1 + tsconfig-paths: 3.15.0 + transitivePeerDependencies: + - eslint-import-resolver-typescript + - eslint-import-resolver-webpack + - supports-color + dev: true + + /eslint-plugin-jest@27.6.0(@typescript-eslint/eslint-plugin@5.62.0)(eslint@8.56.0)(typescript@5.3.3): + resolution: {integrity: sha512-MTlusnnDMChbElsszJvrwD1dN3x6nZl//s4JD23BxB6MgR66TZlL064su24xEIS3VACfAoHV1vgyMgPw8nkdng==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + '@typescript-eslint/eslint-plugin': ^5.0.0 || ^6.0.0 + eslint: ^7.0.0 || ^8.0.0 + jest: '*' + peerDependenciesMeta: + '@typescript-eslint/eslint-plugin': + optional: true + jest: + optional: true + dependencies: + '@typescript-eslint/eslint-plugin': 5.62.0(@typescript-eslint/parser@5.62.0)(eslint@8.56.0)(typescript@5.3.3) + '@typescript-eslint/utils': 5.62.0(eslint@8.56.0)(typescript@5.3.3) + eslint: 8.56.0 + transitivePeerDependencies: + - supports-color + - typescript + dev: true + + /eslint-plugin-jsx-a11y@6.8.0(eslint@8.56.0): + resolution: {integrity: sha512-Hdh937BS3KdwwbBaKd5+PLCOmYY6U4f2h9Z2ktwtNKvIdIEu137rjYbcb9ApSbVJfWxANNuiKTD/9tOKjK9qOA==} + engines: {node: '>=4.0'} + peerDependencies: + eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 + dependencies: + '@babel/runtime': 7.23.6 + aria-query: 5.3.0 + array-includes: 3.1.7 + array.prototype.flatmap: 1.3.2 + ast-types-flow: 0.0.8 + axe-core: 4.7.0 + axobject-query: 3.2.1 + damerau-levenshtein: 1.0.8 + emoji-regex: 9.2.2 + es-iterator-helpers: 1.0.15 + eslint: 8.56.0 + hasown: 2.0.0 + jsx-ast-utils: 3.3.5 + language-tags: 1.0.9 + minimatch: 3.1.2 + object.entries: 1.1.7 + object.fromentries: 2.0.7 + dev: true + + /eslint-plugin-playwright@0.11.2(eslint-plugin-jest@27.6.0)(eslint@8.56.0): + resolution: {integrity: sha512-uRLRLk7uTzc8NE6t4wBU8dijQwHvC66R/h7xwdM779jsJjMUtSmeaB8ayRkkpfwi+UU5BEfwvDANwmE+ccMVDw==} + peerDependencies: + eslint: '>=7' + eslint-plugin-jest: '>=24' + peerDependenciesMeta: + eslint-plugin-jest: + optional: true + dependencies: + eslint: 8.56.0 + eslint-plugin-jest: 27.6.0(@typescript-eslint/eslint-plugin@5.62.0)(eslint@8.56.0)(typescript@5.3.3) + dev: true + + /eslint-plugin-prettier@5.0.0(eslint-config-prettier@8.8.0)(eslint@8.45.0)(prettier@3.0.0): + resolution: {integrity: sha512-AgaZCVuYDXHUGxj/ZGu1u8H8CYgDY3iG6w5kUFw4AzMVXzB7VvbKgYR4nATIN+OvUrghMbiDLeimVjVY5ilq3w==} + engines: {node: ^14.18.0 || >=16.0.0} + peerDependencies: + '@types/eslint': '>=8.0.0' + eslint: '>=8.0.0' + eslint-config-prettier: '*' + prettier: '>=3.0.0' + peerDependenciesMeta: + '@types/eslint': + optional: true + eslint-config-prettier: + optional: true + dependencies: + eslint: 8.45.0 + eslint-config-prettier: 8.8.0(eslint@8.45.0) + prettier: 3.0.0 + prettier-linter-helpers: 1.0.0 + synckit: 0.8.8 + dev: false + + /eslint-plugin-prettier@5.0.0(eslint-config-prettier@8.8.0)(eslint@8.56.0)(prettier@3.0.0): + resolution: {integrity: sha512-AgaZCVuYDXHUGxj/ZGu1u8H8CYgDY3iG6w5kUFw4AzMVXzB7VvbKgYR4nATIN+OvUrghMbiDLeimVjVY5ilq3w==} + engines: {node: ^14.18.0 || >=16.0.0} + peerDependencies: + '@types/eslint': '>=8.0.0' + eslint: '>=8.0.0' + eslint-config-prettier: '*' + prettier: '>=3.0.0' + peerDependenciesMeta: + '@types/eslint': + optional: true + eslint-config-prettier: + optional: true + dependencies: + eslint: 8.56.0 + eslint-config-prettier: 8.8.0(eslint@8.56.0) + prettier: 3.0.0 + prettier-linter-helpers: 1.0.0 + synckit: 0.8.8 + dev: true + + /eslint-plugin-react-hooks@4.6.0(eslint@8.56.0): + resolution: {integrity: sha512-oFc7Itz9Qxh2x4gNHStv3BqJq54ExXmfC+a1NjAta66IAN87Wu0R/QArgIS9qKzX3dXKPI9H5crl9QchNMY9+g==} + engines: {node: '>=10'} + peerDependencies: + eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 + dependencies: + eslint: 8.56.0 + dev: true + + /eslint-plugin-react@7.33.2(eslint@8.56.0): + resolution: {integrity: sha512-73QQMKALArI8/7xGLNI/3LylrEYrlKZSb5C9+q3OtOewTnMQi5cT+aE9E41sLCmli3I9PGGmD1yiZydyo4FEPw==} + engines: {node: '>=4'} + peerDependencies: + eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 + dependencies: + array-includes: 3.1.7 + array.prototype.flatmap: 1.3.2 + array.prototype.tosorted: 1.1.2 + doctrine: 2.1.0 + es-iterator-helpers: 1.0.15 + eslint: 8.56.0 + estraverse: 5.3.0 + jsx-ast-utils: 3.3.5 + minimatch: 3.1.2 + object.entries: 1.1.7 + object.fromentries: 2.0.7 + object.hasown: 1.1.3 + object.values: 1.1.7 + prop-types: 15.8.1 + resolve: 2.0.0-next.5 + semver: 6.3.1 + string.prototype.matchall: 4.0.10 + dev: true + + /eslint-plugin-testing-library@5.11.1(eslint@8.56.0)(typescript@5.3.3): + resolution: {integrity: sha512-5eX9e1Kc2PqVRed3taaLnAAqPZGEX75C+M/rXzUAI3wIg/ZxzUm1OVAwfe/O+vE+6YXOLetSe9g5GKD2ecXipw==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0, npm: '>=6'} + peerDependencies: + eslint: ^7.5.0 || ^8.0.0 + dependencies: + '@typescript-eslint/utils': 5.62.0(eslint@8.56.0)(typescript@5.3.3) + eslint: 8.56.0 + transitivePeerDependencies: + - supports-color + - typescript + dev: true + + /eslint-plugin-tsdoc@0.2.17: + resolution: {integrity: sha512-xRmVi7Zx44lOBuYqG8vzTXuL6IdGOeF9nHX17bjJ8+VE6fsxpdGem0/SBTmAwgYMKYB1WBkqRJVQ+n8GK041pA==} + dependencies: + '@microsoft/tsdoc': 0.14.2 + '@microsoft/tsdoc-config': 0.16.2 + dev: true + + /eslint-plugin-turbo@1.11.2(eslint@8.56.0): + resolution: {integrity: sha512-U6DX+WvgGFiwEAqtOjm4Ejd9O4jsw8jlFNkQi0ywxbMnbiTie+exF4Z0F/B1ajtjjeZkBkgRnlU+UkoraBN+bw==} + peerDependencies: + eslint: '>6.6.0' + dependencies: + dotenv: 16.0.3 + eslint: 8.56.0 + dev: true + + /eslint-plugin-unicorn@43.0.2(eslint@8.56.0): + resolution: {integrity: sha512-DtqZ5mf/GMlfWoz1abIjq5jZfaFuHzGBZYIeuJfEoKKGWRHr2JiJR+ea+BF7Wx2N1PPRoT/2fwgiK1NnmNE3Hg==} + engines: {node: '>=14.18'} + peerDependencies: + eslint: '>=8.18.0' + dependencies: + '@babel/helper-validator-identifier': 7.22.20 + ci-info: 3.9.0 + clean-regexp: 1.0.0 + eslint: 8.56.0 + eslint-utils: 3.0.0(eslint@8.56.0) + esquery: 1.5.0 + indent-string: 4.0.0 + is-builtin-module: 3.2.1 + lodash: 4.17.21 + pluralize: 8.0.0 + read-pkg-up: 7.0.1 + regexp-tree: 0.1.27 + safe-regex: 2.1.1 + semver: 7.5.4 + strip-indent: 3.0.0 + dev: true + + /eslint-scope@5.1.1: + resolution: {integrity: sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==} + engines: {node: '>=8.0.0'} + dependencies: + esrecurse: 4.3.0 + estraverse: 4.3.0 + dev: true + + /eslint-scope@7.2.2: + resolution: {integrity: sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dependencies: + esrecurse: 4.3.0 + estraverse: 5.3.0 + + /eslint-utils@3.0.0(eslint@8.56.0): + resolution: {integrity: sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==} + engines: {node: ^10.0.0 || ^12.0.0 || >= 14.0.0} + peerDependencies: + eslint: '>=5' + dependencies: + eslint: 8.56.0 + eslint-visitor-keys: 2.1.0 + dev: true + + /eslint-visitor-keys@2.1.0: + resolution: {integrity: sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==} + engines: {node: '>=10'} + dev: true + + /eslint-visitor-keys@3.4.3: + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + /eslint@8.45.0: + resolution: {integrity: sha512-pd8KSxiQpdYRfYa9Wufvdoct3ZPQQuVuU5O6scNgMuOMYuxvH0IGaYK0wUFjo4UYYQQCUndlXiMbnxopwvvTiw==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + hasBin: true + dependencies: + '@eslint-community/eslint-utils': 4.4.0(eslint@8.45.0) + '@eslint-community/regexpp': 4.10.0 + '@eslint/eslintrc': 2.1.4 + '@eslint/js': 8.44.0 + '@humanwhocodes/config-array': 0.11.13 + '@humanwhocodes/module-importer': 1.0.1 + '@nodelib/fs.walk': 1.2.8 + ajv: 6.12.6 + chalk: 4.1.2 + cross-spawn: 7.0.3 debug: 4.3.4 - eslint: 8.56.0 + doctrine: 3.0.0 + escape-string-regexp: 4.0.0 + eslint-scope: 7.2.2 + eslint-visitor-keys: 3.4.3 + espree: 9.6.1 + esquery: 1.5.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 6.0.1 + find-up: 5.0.0 + glob-parent: 6.0.2 + globals: 13.24.0 graphemer: 1.4.0 ignore: 5.3.0 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + is-path-inside: 3.0.3 + js-yaml: 4.1.0 + json-stable-stringify-without-jsonify: 1.0.1 + levn: 0.4.1 + lodash.merge: 4.6.2 + minimatch: 3.1.2 natural-compare: 1.4.0 - natural-compare-lite: 1.4.0 - semver: 7.5.4 - ts-api-utils: 1.0.3(typescript@5.3.3) - typescript: 5.3.3 + optionator: 0.9.3 + strip-ansi: 6.0.1 + text-table: 0.2.0 transitivePeerDependencies: - supports-color - dev: true + dev: false - /@typescript-eslint/parser@5.62.0(eslint@8.56.0)(typescript@5.3.3): - resolution: {integrity: sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA==} + /eslint@8.56.0: + resolution: {integrity: sha512-Go19xM6T9puCOWntie1/P997aXxFsOi37JIHRWI514Hc6ZnaHGKY9xFhrU65RT6CcBEzZoGG1e6Nq+DT04ZtZQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - peerDependencies: - eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true + hasBin: true dependencies: - '@typescript-eslint/scope-manager': 5.62.0 - '@typescript-eslint/types': 5.62.0 - '@typescript-eslint/typescript-estree': 5.62.0(typescript@5.3.3) + '@eslint-community/eslint-utils': 4.4.0(eslint@8.56.0) + '@eslint-community/regexpp': 4.10.0 + '@eslint/eslintrc': 2.1.4 + '@eslint/js': 8.56.0 + '@humanwhocodes/config-array': 0.11.14 + '@humanwhocodes/module-importer': 1.0.1 + '@nodelib/fs.walk': 1.2.8 + '@ungap/structured-clone': 1.2.0 + ajv: 6.12.6 + chalk: 4.1.2 + cross-spawn: 7.0.3 debug: 4.3.4 - eslint: 8.56.0 - typescript: 5.3.3 + doctrine: 3.0.0 + escape-string-regexp: 4.0.0 + eslint-scope: 7.2.2 + eslint-visitor-keys: 3.4.3 + espree: 9.6.1 + esquery: 1.5.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 6.0.1 + find-up: 5.0.0 + glob-parent: 6.0.2 + globals: 13.24.0 + graphemer: 1.4.0 + ignore: 5.3.0 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + is-path-inside: 3.0.3 + js-yaml: 4.1.0 + json-stable-stringify-without-jsonify: 1.0.1 + levn: 0.4.1 + lodash.merge: 4.6.2 + minimatch: 3.1.2 + natural-compare: 1.4.0 + optionator: 0.9.3 + strip-ansi: 6.0.1 + text-table: 0.2.0 transitivePeerDependencies: - supports-color dev: true - /@typescript-eslint/parser@6.1.0(eslint@8.56.0)(typescript@5.3.3): - resolution: {integrity: sha512-hIzCPvX4vDs4qL07SYzyomamcs2/tQYXg5DtdAfj35AyJ5PIUqhsLf4YrEIFzZcND7R2E8tpQIZKayxg8/6Wbw==} - engines: {node: ^16.0.0 || >=18.0.0} - peerDependencies: - eslint: ^7.0.0 || ^8.0.0 - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true + /espree@9.6.1: + resolution: {integrity: sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dependencies: - '@typescript-eslint/scope-manager': 6.1.0 - '@typescript-eslint/types': 6.1.0 - '@typescript-eslint/typescript-estree': 6.1.0(typescript@5.3.3) - '@typescript-eslint/visitor-keys': 6.1.0 - debug: 4.3.4 - eslint: 8.56.0 - typescript: 5.3.3 - transitivePeerDependencies: - - supports-color - dev: true + acorn: 8.11.3 + acorn-jsx: 5.3.2(acorn@8.11.3) + eslint-visitor-keys: 3.4.3 - /@typescript-eslint/scope-manager@5.62.0: - resolution: {integrity: sha512-VXuvVvZeQCQb5Zgf4HAxc04q5j+WrNAtNh9OwCsCgpKqESMTu3tF/jhZ3xG6T4NZwWl65Bg8KuS2uEvhSfLl0w==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + /esprima@4.0.1: + resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} + engines: {node: '>=4'} + hasBin: true + + /esquery@1.5.0: + resolution: {integrity: sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==} + engines: {node: '>=0.10'} dependencies: - '@typescript-eslint/types': 5.62.0 - '@typescript-eslint/visitor-keys': 5.62.0 + estraverse: 5.3.0 + + /esrecurse@4.3.0: + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} + dependencies: + estraverse: 5.3.0 + + /estraverse@4.3.0: + resolution: {integrity: sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==} + engines: {node: '>=4.0'} dev: true - /@typescript-eslint/scope-manager@6.1.0: - resolution: {integrity: sha512-AxjgxDn27hgPpe2rQe19k0tXw84YCOsjDJ2r61cIebq1t+AIxbgiXKvD4999Wk49GVaAcdJ/d49FYel+Pp3jjw==} - engines: {node: ^16.0.0 || >=18.0.0} + /estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + + /esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + + /etag@1.8.1: + resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} + engines: {node: '>= 0.6'} + dev: false + + /events@3.3.0: + resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} + engines: {node: '>=0.8.x'} + dev: false + + /execa@5.1.1: + resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==} + engines: {node: '>=10'} dependencies: - '@typescript-eslint/types': 6.1.0 - '@typescript-eslint/visitor-keys': 6.1.0 + cross-spawn: 7.0.3 + get-stream: 6.0.1 + human-signals: 2.1.0 + is-stream: 2.0.1 + merge-stream: 2.0.0 + npm-run-path: 4.0.1 + onetime: 5.1.2 + signal-exit: 3.0.7 + strip-final-newline: 2.0.0 + + /execa@7.2.0: + resolution: {integrity: sha512-UduyVP7TLB5IcAQl+OzLyLcS/l32W/GLg+AhHJ+ow40FOk2U3SAllPwR44v4vmdFwIWqpdwxxpQbF1n5ta9seA==} + engines: {node: ^14.18.0 || ^16.14.0 || >=18.0.0} + dependencies: + cross-spawn: 7.0.3 + get-stream: 6.0.1 + human-signals: 4.3.1 + is-stream: 3.0.0 + merge-stream: 2.0.0 + npm-run-path: 5.2.0 + onetime: 6.0.0 + signal-exit: 3.0.7 + strip-final-newline: 3.0.0 + dev: false + + /execa@8.0.1: + resolution: {integrity: sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==} + engines: {node: '>=16.17'} + dependencies: + cross-spawn: 7.0.3 + get-stream: 8.0.1 + human-signals: 5.0.0 + is-stream: 3.0.0 + merge-stream: 2.0.0 + npm-run-path: 5.2.0 + onetime: 6.0.0 + signal-exit: 4.1.0 + strip-final-newline: 3.0.0 + dev: false + + /exit@0.1.2: + resolution: {integrity: sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==} + engines: {node: '>= 0.8.0'} + + /expand-template@2.0.3: + resolution: {integrity: sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==} + engines: {node: '>=6'} + dev: false + + /expect@28.1.3: + resolution: {integrity: sha512-eEh0xn8HlsuOBxFgIss+2mX85VAS4Qy3OSkjV7rlBWljtA4oWH37glVGyOZSZvErDT/yBywZdPGwCXuTvSG85g==} + engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} + dependencies: + '@jest/expect-utils': 28.1.3 + jest-get-type: 28.0.2 + jest-matcher-utils: 28.1.3 + jest-message-util: 28.1.3 + jest-util: 28.1.3 dev: true - /@typescript-eslint/type-utils@5.62.0(eslint@8.56.0)(typescript@5.3.3): - resolution: {integrity: sha512-xsSQreu+VnfbqQpW5vnCJdq1Z3Q0U31qiWmRhr98ONQmcp/yhiPJFPq8MXiJVLiksmOKSjIldZzkebzHuCGzew==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - peerDependencies: - eslint: '*' - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true + /expect@29.7.0: + resolution: {integrity: sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@typescript-eslint/typescript-estree': 5.62.0(typescript@5.3.3) - '@typescript-eslint/utils': 5.62.0(eslint@8.56.0)(typescript@5.3.3) - debug: 4.3.4 - eslint: 8.56.0 - tsutils: 3.21.0(typescript@5.3.3) - typescript: 5.3.3 + '@jest/expect-utils': 29.7.0 + jest-get-type: 29.6.3 + jest-matcher-utils: 29.7.0 + jest-message-util: 29.7.0 + jest-util: 29.7.0 + + /express@4.18.2: + resolution: {integrity: sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==} + engines: {node: '>= 0.10.0'} + dependencies: + accepts: 1.3.8 + array-flatten: 1.1.1 + body-parser: 1.20.1 + content-disposition: 0.5.4 + content-type: 1.0.5 + cookie: 0.5.0 + cookie-signature: 1.0.6 + debug: 2.6.9 + depd: 2.0.0 + encodeurl: 1.0.2 + escape-html: 1.0.3 + etag: 1.8.1 + finalhandler: 1.2.0 + fresh: 0.5.2 + http-errors: 2.0.0 + merge-descriptors: 1.0.1 + methods: 1.1.2 + on-finished: 2.4.1 + parseurl: 1.3.3 + path-to-regexp: 0.1.7 + proxy-addr: 2.0.7 + qs: 6.11.0 + range-parser: 1.2.1 + safe-buffer: 5.2.1 + send: 0.18.0 + serve-static: 1.15.0 + setprototypeof: 1.2.0 + statuses: 2.0.1 + type-is: 1.6.18 + utils-merge: 1.0.1 + vary: 1.1.2 transitivePeerDependencies: - supports-color - dev: true + dev: false - /@typescript-eslint/type-utils@6.1.0(eslint@8.56.0)(typescript@5.3.3): - resolution: {integrity: sha512-kFXBx6QWS1ZZ5Ni89TyT1X9Ag6RXVIVhqDs0vZE/jUeWlBv/ixq2diua6G7ece6+fXw3TvNRxP77/5mOMusx2w==} - engines: {node: ^16.0.0 || >=18.0.0} - peerDependencies: - eslint: ^7.0.0 || ^8.0.0 - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true + /external-editor@3.1.0: + resolution: {integrity: sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==} + engines: {node: '>=4'} + dependencies: + chardet: 0.7.0 + iconv-lite: 0.4.24 + tmp: 0.0.33 + dev: false + + /extract-zip@2.0.1: + resolution: {integrity: sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==} + engines: {node: '>= 10.17.0'} + hasBin: true dependencies: - '@typescript-eslint/typescript-estree': 6.1.0(typescript@5.3.3) - '@typescript-eslint/utils': 6.1.0(eslint@8.56.0)(typescript@5.3.3) debug: 4.3.4 - eslint: 8.56.0 - ts-api-utils: 1.0.3(typescript@5.3.3) - typescript: 5.3.3 + get-stream: 5.2.0 + yauzl: 2.10.0 + optionalDependencies: + '@types/yauzl': 2.10.3 transitivePeerDependencies: - supports-color - dev: true + dev: false - /@typescript-eslint/types@5.62.0: - resolution: {integrity: sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - dev: true + /fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} - /@typescript-eslint/types@6.1.0: - resolution: {integrity: sha512-+Gfd5NHCpDoHDOaU/yIF3WWRI2PcBRKKpP91ZcVbL0t5tQpqYWBs3z/GGhvU+EV1D0262g9XCnyqQh19prU0JQ==} - engines: {node: ^16.0.0 || >=18.0.0} - dev: true + /fast-diff@1.3.0: + resolution: {integrity: sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==} - /@typescript-eslint/typescript-estree@5.62.0(typescript@5.3.3): - resolution: {integrity: sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - peerDependencies: - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true + /fast-glob@3.3.2: + resolution: {integrity: sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==} + engines: {node: '>=8.6.0'} dependencies: - '@typescript-eslint/types': 5.62.0 - '@typescript-eslint/visitor-keys': 5.62.0 - debug: 4.3.4 - globby: 11.1.0 - is-glob: 4.0.3 - semver: 7.5.4 - tsutils: 3.21.0(typescript@5.3.3) - typescript: 5.3.3 - transitivePeerDependencies: - - supports-color - dev: true + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.5 + + /fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + + /fast-levenshtein@2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + + /fast-safe-stringify@2.1.1: + resolution: {integrity: sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==} + dev: false - /@typescript-eslint/typescript-estree@6.1.0(typescript@5.3.3): - resolution: {integrity: sha512-nUKAPWOaP/tQjU1IQw9sOPCDavs/iU5iYLiY/6u7gxS7oKQoi4aUxXS1nrrVGTyBBaGesjkcwwHkbkiD5eBvcg==} - engines: {node: ^16.0.0 || >=18.0.0} - peerDependencies: - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true + /fast-xml-parser@4.1.2: + resolution: {integrity: sha512-CDYeykkle1LiA/uqQyNwYpFbyF6Axec6YapmpUP+/RHWIoR1zKjocdvNaTsxCxZzQ6v9MLXaSYm9Qq0thv0DHg==} + hasBin: true dependencies: - '@typescript-eslint/types': 6.1.0 - '@typescript-eslint/visitor-keys': 6.1.0 - debug: 4.3.4 - globby: 11.1.0 - is-glob: 4.0.3 - semver: 7.5.4 - ts-api-utils: 1.0.3(typescript@5.3.3) - typescript: 5.3.3 - transitivePeerDependencies: - - supports-color - dev: true + strnum: 1.0.5 + dev: false - /@typescript-eslint/utils@5.62.0(eslint@8.56.0)(typescript@5.3.3): - resolution: {integrity: sha512-n8oxjeb5aIbPFEtmQxQYOLI0i9n5ySBEY/ZEHHZqKQSFnxio1rv6dthascc9dLuwrL0RC5mPCxB7vnAVGAYWAQ==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - peerDependencies: - eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 + /fast-xml-parser@4.2.5: + resolution: {integrity: sha512-B9/wizE4WngqQftFPmdaMYlXoJlJOYxGQOanC77fq9k8+Z0v5dDSVh+3glErdIROP//s/jgb7ZuxKfB8nVyo0g==} + hasBin: true dependencies: - '@eslint-community/eslint-utils': 4.4.0(eslint@8.56.0) - '@types/json-schema': 7.0.15 - '@types/semver': 7.5.6 - '@typescript-eslint/scope-manager': 5.62.0 - '@typescript-eslint/types': 5.62.0 - '@typescript-eslint/typescript-estree': 5.62.0(typescript@5.3.3) - eslint: 8.56.0 - eslint-scope: 5.1.1 - semver: 7.5.4 - transitivePeerDependencies: - - supports-color - - typescript - dev: true + strnum: 1.0.5 + dev: false - /@typescript-eslint/utils@6.1.0(eslint@8.56.0)(typescript@5.3.3): - resolution: {integrity: sha512-wp652EogZlKmQoMS5hAvWqRKplXvkuOnNzZSE0PVvsKjpexd/XznRVHAtrfHFYmqaJz0DFkjlDsGYC9OXw+OhQ==} - engines: {node: ^16.0.0 || >=18.0.0} - peerDependencies: - eslint: ^7.0.0 || ^8.0.0 + /fastq@1.16.0: + resolution: {integrity: sha512-ifCoaXsDrsdkWTtiNJX5uzHDsrck5TzfKKDcuFFTIrrc/BS076qgEIfoIy1VeZqViznfKiysPYTh/QeHtnIsYA==} dependencies: - '@eslint-community/eslint-utils': 4.4.0(eslint@8.56.0) - '@types/json-schema': 7.0.15 - '@types/semver': 7.5.6 - '@typescript-eslint/scope-manager': 6.1.0 - '@typescript-eslint/types': 6.1.0 - '@typescript-eslint/typescript-estree': 6.1.0(typescript@5.3.3) - eslint: 8.56.0 - semver: 7.5.4 - transitivePeerDependencies: - - supports-color - - typescript - dev: true + reusify: 1.0.4 - /@typescript-eslint/visitor-keys@5.62.0: - resolution: {integrity: sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + /fb-watchman@2.0.2: + resolution: {integrity: sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==} dependencies: - '@typescript-eslint/types': 5.62.0 - eslint-visitor-keys: 3.4.3 - dev: true + bser: 2.1.1 - /@typescript-eslint/visitor-keys@6.1.0: - resolution: {integrity: sha512-yQeh+EXhquh119Eis4k0kYhj9vmFzNpbhM3LftWQVwqVjipCkwHBQOZutcYW+JVkjtTG9k8nrZU1UoNedPDd1A==} - engines: {node: ^16.0.0 || >=18.0.0} + /fd-slicer@1.1.0: + resolution: {integrity: sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==} dependencies: - '@typescript-eslint/types': 6.1.0 - eslint-visitor-keys: 3.4.3 - dev: true + pend: 1.2.0 + dev: false - /@ungap/structured-clone@1.2.0: - resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==} - dev: true + /fecha@4.2.3: + resolution: {integrity: sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==} + dev: false - /@vercel/style-guide@4.0.2(eslint@8.56.0)(prettier@3.0.0)(typescript@5.3.3): - resolution: {integrity: sha512-FroL+oOePzhw7n/I+f7zr4WNroGHT/+2TlW6WH9+CVSjMNsEyu7Qstj2mI5gWIBjT1Y2ZImKPppCzI2cIYmNZw==} - engines: {node: '>=16'} - peerDependencies: - '@next/eslint-plugin-next': ^12.3.0 - eslint: ^8.24.0 - prettier: ^2.7.0 - typescript: ^4.8.0 - peerDependenciesMeta: - '@next/eslint-plugin-next': - optional: true - eslint: - optional: true - prettier: - optional: true - typescript: - optional: true + /figures@2.0.0: + resolution: {integrity: sha512-Oa2M9atig69ZkfwiApY8F2Yy+tzMbazyvqv21R0NsSC8floSOC09BbT1ITWAdoMGQvJ/aZnR1KMwdx9tvHnTNA==} + engines: {node: '>=4'} dependencies: - '@babel/core': 7.23.6 - '@babel/eslint-parser': 7.23.3(@babel/core@7.23.6)(eslint@8.56.0) - '@rushstack/eslint-patch': 1.6.1 - '@typescript-eslint/eslint-plugin': 5.62.0(@typescript-eslint/parser@5.62.0)(eslint@8.56.0)(typescript@5.3.3) - '@typescript-eslint/parser': 5.62.0(eslint@8.56.0)(typescript@5.3.3) - eslint: 8.56.0 - eslint-config-prettier: 8.8.0(eslint@8.56.0) - eslint-import-resolver-alias: 1.1.2(eslint-plugin-import@2.29.1) - eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@5.62.0)(eslint-plugin-import@2.29.1)(eslint@8.56.0) - eslint-plugin-eslint-comments: 3.2.0(eslint@8.56.0) - eslint-plugin-import: 2.29.1(@typescript-eslint/parser@6.1.0)(eslint@8.56.0) - eslint-plugin-jest: 27.6.0(@typescript-eslint/eslint-plugin@5.62.0)(eslint@8.56.0)(typescript@5.3.3) - eslint-plugin-jsx-a11y: 6.8.0(eslint@8.56.0) - eslint-plugin-playwright: 0.11.2(eslint-plugin-jest@27.6.0)(eslint@8.56.0) - eslint-plugin-react: 7.33.2(eslint@8.56.0) - eslint-plugin-react-hooks: 4.6.0(eslint@8.56.0) - eslint-plugin-testing-library: 5.11.1(eslint@8.56.0)(typescript@5.3.3) - eslint-plugin-tsdoc: 0.2.17 - eslint-plugin-unicorn: 43.0.2(eslint@8.56.0) - prettier: 3.0.0 - prettier-plugin-packagejson: 2.4.8(prettier@3.0.0) - typescript: 5.3.3 - transitivePeerDependencies: - - eslint-import-resolver-node - - eslint-import-resolver-webpack - - jest - - supports-color - dev: true + escape-string-regexp: 1.0.5 + dev: false - /acorn-jsx@5.3.2(acorn@8.11.2): - resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} - peerDependencies: - acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + /figures@3.2.0: + resolution: {integrity: sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==} + engines: {node: '>=8'} dependencies: - acorn: 8.11.2 - dev: true + escape-string-regexp: 1.0.5 + dev: false - /acorn@8.11.2: - resolution: {integrity: sha512-nc0Axzp/0FILLEVsm4fNwLCwMttvhEI263QtVPQcbpfZZ3ts0hLsZGOpE6czNlid7CJ9MlyH8reXkpsf3YUY4w==} - engines: {node: '>=0.4.0'} - hasBin: true - dev: true + /figures@5.0.0: + resolution: {integrity: sha512-ej8ksPF4x6e5wvK9yevct0UCXh8TTFlWGVLlgjZuoBH1HwjIfKE/IdL5mq89sFA7zELi1VhKpmtDnrs7zWyeyg==} + engines: {node: '>=14'} + dependencies: + escape-string-regexp: 5.0.0 + is-unicode-supported: 1.3.0 + dev: false - /ajv@6.12.6: - resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} + /file-entry-cache@6.0.1: + resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==} + engines: {node: ^10.12.0 || >=12.0.0} dependencies: - fast-deep-equal: 3.1.3 - fast-json-stable-stringify: 2.1.0 - json-schema-traverse: 0.4.1 - uri-js: 4.4.1 - dev: true + flat-cache: 3.2.0 - /ansi-regex@5.0.1: - resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + /fill-range@7.0.1: + resolution: {integrity: sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==} engines: {node: '>=8'} - dev: true + dependencies: + to-regex-range: 5.0.1 - /ansi-styles@3.2.1: - resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==} + /finalhandler@1.2.0: + resolution: {integrity: sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==} + engines: {node: '>= 0.8'} + dependencies: + debug: 2.6.9 + encodeurl: 1.0.2 + escape-html: 1.0.3 + on-finished: 2.4.1 + parseurl: 1.3.3 + statuses: 2.0.1 + unpipe: 1.0.0 + transitivePeerDependencies: + - supports-color + dev: false + + /find-up-simple@1.0.0: + resolution: {integrity: sha512-q7Us7kcjj2VMePAa02hDAF6d+MzsdsAWEwYyOpwUtlerRBkOEPBCRZrAV4XfcSN8fHAgaD0hP7miwoay6DCprw==} + engines: {node: '>=18'} + dev: false + + /find-up@2.1.0: + resolution: {integrity: sha512-NWzkk0jSJtTt08+FBFMvXoeZnOJD+jTtsRmBYbAIzJdX6l7dLgR7CTubCM5/eDdPUBvLCeVasP1brfVR/9/EZQ==} engines: {node: '>=4'} dependencies: - color-convert: 1.9.3 - dev: true + locate-path: 2.0.0 + dev: false - /ansi-styles@4.3.0: - resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + /find-up@3.0.0: + resolution: {integrity: sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==} + engines: {node: '>=6'} + dependencies: + locate-path: 3.0.0 + dev: false + + /find-up@4.1.0: + resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} engines: {node: '>=8'} dependencies: - color-convert: 2.0.1 - dev: true + locate-path: 5.0.0 + path-exists: 4.0.0 - /argparse@2.0.1: - resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} - dev: true + /find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 - /aria-query@5.3.0: - resolution: {integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==} + /find-up@6.3.0: + resolution: {integrity: sha512-v2ZsoEuVHYy8ZIlYqwPe/39Cy+cFDzp4dXPaxNvkEuouymu+2Jbz0PxpKarJHYJTmv2HWT3O382qY8l4jMWthw==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} dependencies: - dequal: 2.0.3 - dev: true + locate-path: 7.2.0 + path-exists: 5.0.0 + dev: false - /array-buffer-byte-length@1.0.0: - resolution: {integrity: sha512-LPuwb2P+NrQw3XhxGc36+XSvuBPopovXYTR9Ew++Du9Yb/bx5AzBfrIsBoj0EZUifjQU+sHL21sseZ3jerWO/A==} + /find-versions@5.1.0: + resolution: {integrity: sha512-+iwzCJ7C5v5KgcBuueqVoNiHVoQpwiUK5XFLjf0affFTep+Wcw93tPvmb8tqujDNmzhBDPddnWV/qgWSXgq+Hg==} + engines: {node: '>=12'} dependencies: - call-bind: 1.0.5 - is-array-buffer: 3.0.2 - dev: true + semver-regex: 4.0.5 + dev: false - /array-includes@3.1.7: - resolution: {integrity: sha512-dlcsNBIiWhPkHdOEEKnehA+RNUWDc4UqFtnIXU4uuYDPtA4LDkr7qip2p0VvFAEXNDr0yWZ9PJyIRiGjRLQzwQ==} - engines: {node: '>= 0.4'} + /flat-cache@3.2.0: + resolution: {integrity: sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==} + engines: {node: ^10.12.0 || >=12.0.0} dependencies: - call-bind: 1.0.5 - define-properties: 1.2.1 - es-abstract: 1.22.3 - get-intrinsic: 1.2.2 - is-string: 1.0.7 - dev: true + flatted: 3.2.9 + keyv: 4.5.4 + rimraf: 3.0.2 - /array-union@2.1.0: - resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} - engines: {node: '>=8'} - dev: true + /flatted@3.2.9: + resolution: {integrity: sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==} + + /fn.name@1.1.0: + resolution: {integrity: sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==} + dev: false + + /follow-redirects@1.15.4: + resolution: {integrity: sha512-Cr4D/5wlrb0z9dgERpUL3LrmPKVDsETIJhaCMeDfuFYcqa5bldGV6wBsAN6X/vxlXQtFBMrXdXxdL8CbDTGniw==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + dev: false + + /for-each@0.3.3: + resolution: {integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==} + dependencies: + is-callable: 1.2.7 + + /foreground-child@3.1.1: + resolution: {integrity: sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==} + engines: {node: '>=14'} + dependencies: + cross-spawn: 7.0.3 + signal-exit: 4.1.0 + dev: false + + /form-data@4.0.0: + resolution: {integrity: sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==} + engines: {node: '>= 6'} + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + mime-types: 2.1.35 + dev: false + + /formidable@2.1.2: + resolution: {integrity: sha512-CM3GuJ57US06mlpQ47YcunuUZ9jpm8Vx+P2CGt2j7HpgkKZO/DJYQ0Bobim8G6PFQmK5lOqOOdUXboU+h73A4g==} + dependencies: + dezalgo: 1.0.4 + hexoid: 1.0.0 + once: 1.4.0 + qs: 6.11.2 + dev: false + + /forwarded@0.2.0: + resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} + engines: {node: '>= 0.6'} + dev: false + + /fresh@0.5.2: + resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==} + engines: {node: '>= 0.6'} + dev: false + + /from2@2.3.0: + resolution: {integrity: sha512-OMcX/4IC/uqEPVgGeyfN22LJk6AZrMkRZHxcHBMBvHScDGgwTm2GT2Wkgtocyd3JfZffjj2kYUDXXII0Fk9W0g==} + dependencies: + inherits: 2.0.4 + readable-stream: 2.3.8 + dev: false - /array.prototype.findlastindex@1.2.3: - resolution: {integrity: sha512-LzLoiOMAxvy+Gd3BAq3B7VeIgPdo+Q8hthvKtXybMvRV0jrXfJM/t8mw7nNlpEcVlVUnCnM2KSX4XU5HmpodOA==} - engines: {node: '>= 0.4'} + /fs-constants@1.0.0: + resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==} + dev: false + + /fs-extra@10.1.0: + resolution: {integrity: sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==} + engines: {node: '>=12'} dependencies: - call-bind: 1.0.5 - define-properties: 1.2.1 - es-abstract: 1.22.3 - es-shim-unscopables: 1.0.2 - get-intrinsic: 1.2.2 - dev: true + graceful-fs: 4.2.11 + jsonfile: 6.1.0 + universalify: 2.0.1 + dev: false - /array.prototype.flat@1.3.2: - resolution: {integrity: sha512-djYB+Zx2vLewY8RWlNCUdHjDXs2XOgm602S9E7P/UpHgfeHL00cRiIF+IN/G/aUJ7kGPb6yO/ErDI5V2s8iycA==} - engines: {node: '>= 0.4'} + /fs-extra@11.2.0: + resolution: {integrity: sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==} + engines: {node: '>=14.14'} dependencies: - call-bind: 1.0.5 - define-properties: 1.2.1 - es-abstract: 1.22.3 - es-shim-unscopables: 1.0.2 - dev: true + graceful-fs: 4.2.11 + jsonfile: 6.1.0 + universalify: 2.0.1 + dev: false - /array.prototype.flatmap@1.3.2: - resolution: {integrity: sha512-Ewyx0c9PmpcsByhSW4r+9zDU7sGjFc86qf/kKtuSCRdhfbk0SNLLkaT5qvcHnRGgc5NP/ly/y+qkXkqONX54CQ==} - engines: {node: '>= 0.4'} + /fs-extra@8.1.0: + resolution: {integrity: sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==} + engines: {node: '>=6 <7 || >=8'} dependencies: - call-bind: 1.0.5 - define-properties: 1.2.1 - es-abstract: 1.22.3 - es-shim-unscopables: 1.0.2 - dev: true + graceful-fs: 4.2.11 + jsonfile: 4.0.0 + universalify: 0.1.2 + dev: false - /array.prototype.tosorted@1.1.2: - resolution: {integrity: sha512-HuQCHOlk1Weat5jzStICBCd83NxiIMwqDg/dHEsoefabn/hJRj5pVdWcPUSpRrwhwxZOsQassMpgN/xRYFBMIg==} + /fs-extra@9.1.0: + resolution: {integrity: sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==} + engines: {node: '>=10'} dependencies: - call-bind: 1.0.5 - define-properties: 1.2.1 - es-abstract: 1.22.3 - es-shim-unscopables: 1.0.2 - get-intrinsic: 1.2.2 - dev: true + at-least-node: 1.0.0 + graceful-fs: 4.2.11 + jsonfile: 6.1.0 + universalify: 2.0.1 + dev: false - /arraybuffer.prototype.slice@1.0.2: - resolution: {integrity: sha512-yMBKppFur/fbHu9/6USUe03bZ4knMYiwFBcyiaXB8Go0qNehwX6inYPzK9U0NeQvGxKthcmHcaR8P5MStSRBAw==} + /fs.realpath@1.0.0: + resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + + /fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + requiresBuild: true + optional: true + + /function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + /function.prototype.name@1.1.6: + resolution: {integrity: sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg==} engines: {node: '>= 0.4'} dependencies: - array-buffer-byte-length: 1.0.0 call-bind: 1.0.5 define-properties: 1.2.1 es-abstract: 1.22.3 - get-intrinsic: 1.2.2 - is-array-buffer: 3.0.2 - is-shared-array-buffer: 1.0.2 + functions-have-names: 1.2.3 dev: true - /ast-types-flow@0.0.8: - resolution: {integrity: sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==} - dev: true + /functions-have-names@1.2.3: + resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} - /asynciterator.prototype@1.0.0: - resolution: {integrity: sha512-wwHYEIS0Q80f5mosx3L/dfG5t5rjEa9Ft51GTaNt862EnpyGHpgz2RkZvLPp1oF5TnAiTohkEKVEu8pQPJI7Vg==} - dependencies: - has-symbols: 1.0.3 - dev: true + /gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} - /available-typed-arrays@1.0.5: - resolution: {integrity: sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==} - engines: {node: '>= 0.4'} - dev: true + /get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} - /axe-core@4.7.0: - resolution: {integrity: sha512-M0JtH+hlOL5pLQwHOLNYZaXuhqmvS8oExsqB1SBYgA4Dk7u/xx+YdGHXaK5pyUfed5mYXdlYiphWq3G8cRi5JQ==} - engines: {node: '>=4'} + /get-func-name@2.0.2: + resolution: {integrity: sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==} dev: true - /axobject-query@3.2.1: - resolution: {integrity: sha512-jsyHu61e6N4Vbz/v18DHwWYKK0bSWLqn47eeDSKPB7m8tqMHF9YJ+mhIk2lVteyZrY8tnSj/jHOv4YiTCuCJgg==} + /get-intrinsic@1.2.2: + resolution: {integrity: sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA==} dependencies: - dequal: 2.0.3 - dev: true + function-bind: 1.1.2 + has-proto: 1.0.1 + has-symbols: 1.0.3 + hasown: 2.0.0 - /balanced-match@1.0.2: - resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} - dev: true + /get-package-type@0.1.0: + resolution: {integrity: sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==} + engines: {node: '>=8.0.0'} - /brace-expansion@1.1.11: - resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} - dependencies: - balanced-match: 1.0.2 - concat-map: 0.0.1 + /get-stdin@9.0.0: + resolution: {integrity: sha512-dVKBjfWisLAicarI2Sf+JuBE/DghV4UzNAVe9yhEJuzeREd3JhOTE9cUaJTeSa77fsbQUK3pcOpJfM59+VKZaA==} + engines: {node: '>=12'} dev: true - /braces@3.0.2: - resolution: {integrity: sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==} + /get-stream@5.2.0: + resolution: {integrity: sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==} engines: {node: '>=8'} dependencies: - fill-range: 7.0.1 - dev: true + pump: 3.0.0 + dev: false - /browserslist@4.22.2: - resolution: {integrity: sha512-0UgcrvQmBDvZHFGdYUehrCNIazki7/lUP3kkoi/r3YB2amZbFM9J43ZRkJTXBUZK4gmx56+Sqk9+Vs9mwZx9+A==} - engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} - hasBin: true - dependencies: - caniuse-lite: 1.0.30001571 - electron-to-chromium: 1.4.616 - node-releases: 2.0.14 - update-browserslist-db: 1.0.13(browserslist@4.22.2) - dev: true + /get-stream@6.0.1: + resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==} + engines: {node: '>=10'} - /builtin-modules@3.3.0: - resolution: {integrity: sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==} - engines: {node: '>=6'} - dev: true + /get-stream@7.0.1: + resolution: {integrity: sha512-3M8C1EOFN6r8AMUhwUAACIoXZJEOufDU5+0gFFN5uNs6XYOralD2Pqkl7m046va6x77FwposWXbAhPPIOus7mQ==} + engines: {node: '>=16'} + dev: false - /call-bind@1.0.5: - resolution: {integrity: sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==} + /get-stream@8.0.1: + resolution: {integrity: sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==} + engines: {node: '>=16'} + dev: false + + /get-symbol-description@1.0.0: + resolution: {integrity: sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==} + engines: {node: '>= 0.4'} dependencies: - function-bind: 1.1.2 + call-bind: 1.0.5 get-intrinsic: 1.2.2 - set-function-length: 1.1.1 - dev: true - - /callsites@3.1.0: - resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} - engines: {node: '>=6'} dev: true - /caniuse-lite@1.0.30001571: - resolution: {integrity: sha512-tYq/6MoXhdezDLFZuCO/TKboTzuQ/xR5cFdgXPfDtM7/kchBO3b4VWghE/OAi/DV7tTdhmLjZiZBZi1fA/GheQ==} + /get-tsconfig@4.7.2: + resolution: {integrity: sha512-wuMsz4leaj5hbGgg4IvDU0bqJagpftG5l5cXIAvo8uZrqn0NJqwtfupTN00VnkQJPcIRrxYrm1Ue24btpCha2A==} + dependencies: + resolve-pkg-maps: 1.0.0 dev: true - /chalk@2.4.2: - resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==} - engines: {node: '>=4'} - dependencies: - ansi-styles: 3.2.1 - escape-string-regexp: 1.0.5 - supports-color: 5.5.0 + /git-hooks-list@3.1.0: + resolution: {integrity: sha512-LF8VeHeR7v+wAbXqfgRlTSX/1BJR9Q1vEMR8JAz1cEg6GX07+zyj3sAdDvYjj/xnlIfVuGgj4qBei1K3hKH+PA==} dev: true - /chalk@4.1.2: - resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} - engines: {node: '>=10'} + /git-log-parser@1.2.0: + resolution: {integrity: sha512-rnCVNfkTL8tdNryFuaY0fYiBWEBcgF748O6ZI61rslBvr2o7U65c2/6npCRqH40vuAhtgtDiqLTJjBVdrejCzA==} dependencies: - ansi-styles: 4.3.0 - supports-color: 7.2.0 - dev: true + argv-formatter: 1.0.0 + spawn-error-forwarder: 1.0.0 + split2: 1.0.0 + stream-combiner2: 1.1.1 + through2: 2.0.5 + traverse: 0.6.8 + dev: false - /ci-info@3.9.0: - resolution: {integrity: sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==} - engines: {node: '>=8'} - dev: true + /github-from-package@0.0.0: + resolution: {integrity: sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==} + dev: false - /clean-regexp@1.0.0: - resolution: {integrity: sha512-GfisEZEJvzKrmGWkvfhgzcz/BllN1USeqD2V6tg14OAOgaCD2Z/PUEuxnAZ/nPvmaHRG7a8y77p1T/IRQ4D1Hw==} - engines: {node: '>=4'} + /glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} dependencies: - escape-string-regexp: 1.0.5 - dev: true + is-glob: 4.0.3 - /color-convert@1.9.3: - resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} + /glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} dependencies: - color-name: 1.1.3 - dev: true + is-glob: 4.0.3 - /color-convert@2.0.1: - resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} - engines: {node: '>=7.0.0'} + /glob@10.3.10: + resolution: {integrity: sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==} + engines: {node: '>=16 || 14 >=14.17'} + hasBin: true dependencies: - color-name: 1.1.4 - dev: true - - /color-name@1.1.3: - resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==} - dev: true - - /color-name@1.1.4: - resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} - dev: true + foreground-child: 3.1.1 + jackspeak: 2.3.6 + minimatch: 9.0.3 + minipass: 7.0.4 + path-scurry: 1.10.1 + dev: false - /concat-map@0.0.1: - resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} - dev: true + /glob@7.2.3: + resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 3.1.2 + once: 1.4.0 + path-is-absolute: 1.0.1 - /convert-source-map@2.0.0: - resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} - dev: true + /globals@11.12.0: + resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==} + engines: {node: '>=4'} - /cross-spawn@7.0.3: - resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} - engines: {node: '>= 8'} + /globals@13.24.0: + resolution: {integrity: sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==} + engines: {node: '>=8'} dependencies: - path-key: 3.1.1 - shebang-command: 2.0.0 - which: 2.0.2 + type-fest: 0.20.2 - /damerau-levenshtein@1.0.8: - resolution: {integrity: sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==} + /globalthis@1.0.3: + resolution: {integrity: sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA==} + engines: {node: '>= 0.4'} + dependencies: + define-properties: 1.2.1 dev: true - /debug@3.2.7: - resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==} - peerDependencies: - supports-color: '*' - peerDependenciesMeta: - supports-color: - optional: true + /globby@11.1.0: + resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} + engines: {node: '>=10'} dependencies: - ms: 2.1.3 - dev: true + array-union: 2.1.0 + dir-glob: 3.0.1 + fast-glob: 3.3.2 + ignore: 5.3.0 + merge2: 1.4.1 + slash: 3.0.0 - /debug@4.3.4: - resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} - engines: {node: '>=6.0'} - peerDependencies: - supports-color: '*' - peerDependenciesMeta: - supports-color: - optional: true + /globby@13.2.2: + resolution: {integrity: sha512-Y1zNGV+pzQdh7H39l9zgB4PJqjRNqydvdYCDG4HFXM4XuvSaQQlEc91IU1yALL8gUTDomgBAfz3XJdmUS+oo0w==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} dependencies: - ms: 2.1.2 + dir-glob: 3.0.1 + fast-glob: 3.3.2 + ignore: 5.3.0 + merge2: 1.4.1 + slash: 4.0.0 dev: true - /deep-is@0.1.4: - resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} - dev: true + /globby@14.0.0: + resolution: {integrity: sha512-/1WM/LNHRAOH9lZta77uGbq0dAEQM+XjNesWwhlERDVenqothRbnzTrL3/LrIoEPPjeUHC3vrS6TwoyxeHs7MQ==} + engines: {node: '>=18'} + dependencies: + '@sindresorhus/merge-streams': 1.0.0 + fast-glob: 3.3.2 + ignore: 5.3.0 + path-type: 5.0.0 + slash: 5.1.0 + unicorn-magic: 0.1.0 + dev: false - /define-data-property@1.1.1: - resolution: {integrity: sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==} - engines: {node: '>= 0.4'} + /gopd@1.0.1: + resolution: {integrity: sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==} dependencies: get-intrinsic: 1.2.2 - gopd: 1.0.1 - has-property-descriptors: 1.0.1 - dev: true - /define-properties@1.2.1: - resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} - engines: {node: '>= 0.4'} - dependencies: - define-data-property: 1.1.1 - has-property-descriptors: 1.0.1 - object-keys: 1.1.1 - dev: true + /got@11.8.6: + resolution: {integrity: sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==} + engines: {node: '>=10.19.0'} + dependencies: + '@sindresorhus/is': 4.6.0 + '@szmarczak/http-timer': 4.0.6 + '@types/cacheable-request': 6.0.3 + '@types/responselike': 1.0.3 + cacheable-lookup: 5.0.4 + cacheable-request: 7.0.4 + decompress-response: 6.0.0 + http2-wrapper: 1.0.3 + lowercase-keys: 2.0.0 + p-cancelable: 2.1.1 + responselike: 2.0.1 + dev: false - /dequal@2.0.3: - resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} - engines: {node: '>=6'} - dev: true + /graceful-fs@4.2.10: + resolution: {integrity: sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==} + dev: false - /detect-indent@7.0.1: - resolution: {integrity: sha512-Mc7QhQ8s+cLrnUfU/Ji94vG/r8M26m8f++vyres4ZoojaRDpZ1eSIh/EpzLNwlWuvzSZ3UbDFspjFvTDXe6e/g==} - engines: {node: '>=12.20'} - dev: true + /graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} - /detect-newline@4.0.1: - resolution: {integrity: sha512-qE3Veg1YXzGHQhlA6jzebZN2qVf6NX+A7m7qlhCGG30dJixrAQhYOsJjsnBjJkCSmuOPpCk30145fr8FV0bzog==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - dev: true + /graphemer@1.4.0: + resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} - /dir-glob@3.0.1: - resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} - engines: {node: '>=8'} - dependencies: - path-type: 4.0.0 - dev: true + /graphology-types@0.24.7: + resolution: {integrity: sha512-tdcqOOpwArNjEr0gNQKCXwaNCWnQJrog14nJNQPeemcLnXQUUGrsCWpWkVKt46zLjcS6/KGoayeJfHHyPDlvwA==} + dev: false - /doctrine@2.1.0: - resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==} - engines: {node: '>=0.10.0'} + /graphology@0.25.4(graphology-types@0.24.7): + resolution: {integrity: sha512-33g0Ol9nkWdD6ulw687viS8YJQBxqG5LWII6FI6nul0pq6iM2t5EKquOTFDbyTblRB3O9I+7KX4xI8u5ffekAQ==} + peerDependencies: + graphology-types: '>=0.24.0' dependencies: - esutils: 2.0.3 - dev: true + events: 3.3.0 + graphology-types: 0.24.7 + obliterator: 2.0.4 + dev: false - /doctrine@3.0.0: - resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==} - engines: {node: '>=6.0.0'} + /graphql-tag@2.12.6(graphql@16.8.1): + resolution: {integrity: sha512-FdSNcu2QQcWnM2VNvSCCDCVS5PpPqpzgFT8+GXzqJuoDd0CBncxCY278u4mhRO7tMgo2JjgJA5aZ+nWSQ/Z+xg==} + engines: {node: '>=10'} + peerDependencies: + graphql: ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 dependencies: - esutils: 2.0.3 - dev: true + graphql: 16.8.1 + tslib: 2.6.2 + dev: false - /dotenv-cli@7.3.0: - resolution: {integrity: sha512-314CA4TyK34YEJ6ntBf80eUY+t1XaFLyem1k9P0sX1gn30qThZ5qZr/ZwE318gEnzyYP9yj9HJk6SqwE0upkfw==} + /graphql@16.8.1: + resolution: {integrity: sha512-59LZHPdGZVh695Ud9lRzPBVTtlX9ZCV150Er2W43ro37wVof0ctenSaskPPjN7lVTIN8mSZt8PHUNKZuNQUuxw==} + engines: {node: ^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0} + dev: false + + /handlebars@4.7.8: + resolution: {integrity: sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==} + engines: {node: '>=0.4.7'} hasBin: true dependencies: - cross-spawn: 7.0.3 - dotenv: 16.3.1 - dotenv-expand: 10.0.0 minimist: 1.2.8 + neo-async: 2.6.2 + source-map: 0.6.1 + wordwrap: 1.0.0 + optionalDependencies: + uglify-js: 3.17.4 dev: false - /dotenv-expand@10.0.0: - resolution: {integrity: sha512-GopVGCpVS1UKH75VKHGuQFqS1Gusej0z4FyQkPdwjil2gNIv+LNsqBlboOzpJFZKVT95GkCyWJbBSdFEFUWI2A==} - engines: {node: '>=12'} + /hard-rejection@2.1.0: + resolution: {integrity: sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA==} + engines: {node: '>=6'} dev: false - /dotenv@16.0.3: - resolution: {integrity: sha512-7GO6HghkA5fYG9TYnNxi14/7K9f5occMlp3zXAuSxn7CKCxt9xbNWG7yF8hTCSUchlfWSe3uLmlPfigevRItzQ==} - engines: {node: '>=12'} - dev: true - - /dotenv@16.3.1: - resolution: {integrity: sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ==} - engines: {node: '>=12'} - dev: false + /has-bigints@1.0.2: + resolution: {integrity: sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==} - /electron-to-chromium@1.4.616: - resolution: {integrity: sha512-1n7zWYh8eS0L9Uy+GskE0lkBUNK83cXTVJI0pU3mGprFsbfSdAc15VTFbo+A+Bq4pwstmL30AVcEU3Fo463lNg==} - dev: true + /has-flag@3.0.0: + resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} + engines: {node: '>=4'} - /emoji-regex@9.2.2: - resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} - dev: true + /has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} - /enhanced-resolve@5.15.0: - resolution: {integrity: sha512-LXYT42KJ7lpIKECr2mAXIaMldcNCh/7E0KBKOu4KSfkHmP+mZmSs+8V5gBAqisWBy0OO4W5Oyys0GO1Y8KtdKg==} - engines: {node: '>=10.13.0'} + /has-property-descriptors@1.0.1: + resolution: {integrity: sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==} dependencies: - graceful-fs: 4.2.11 - tapable: 2.2.1 - dev: true + get-intrinsic: 1.2.2 - /error-ex@1.3.2: - resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==} - dependencies: - is-arrayish: 0.2.1 - dev: true + /has-proto@1.0.1: + resolution: {integrity: sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==} + engines: {node: '>= 0.4'} - /es-abstract@1.22.3: - resolution: {integrity: sha512-eiiY8HQeYfYH2Con2berK+To6GrK2RxbPawDkGq4UiCQQfZHb6wX9qQqkbpPqaxQFcl8d9QzZqo0tGE0VcrdwA==} + /has-symbols@1.0.3: + resolution: {integrity: sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==} engines: {node: '>= 0.4'} - dependencies: - array-buffer-byte-length: 1.0.0 - arraybuffer.prototype.slice: 1.0.2 - available-typed-arrays: 1.0.5 - call-bind: 1.0.5 - es-set-tostringtag: 2.0.2 - es-to-primitive: 1.2.1 - function.prototype.name: 1.1.6 - get-intrinsic: 1.2.2 - get-symbol-description: 1.0.0 - globalthis: 1.0.3 - gopd: 1.0.1 - has-property-descriptors: 1.0.1 - has-proto: 1.0.1 - has-symbols: 1.0.3 - hasown: 2.0.0 - internal-slot: 1.0.6 - is-array-buffer: 3.0.2 - is-callable: 1.2.7 - is-negative-zero: 2.0.2 - is-regex: 1.1.4 - is-shared-array-buffer: 1.0.2 - is-string: 1.0.7 - is-typed-array: 1.1.12 - is-weakref: 1.0.2 - object-inspect: 1.13.1 - object-keys: 1.1.1 - object.assign: 4.1.5 - regexp.prototype.flags: 1.5.1 - safe-array-concat: 1.0.1 - safe-regex-test: 1.0.0 - string.prototype.trim: 1.2.8 - string.prototype.trimend: 1.0.7 - string.prototype.trimstart: 1.0.7 - typed-array-buffer: 1.0.0 - typed-array-byte-length: 1.0.0 - typed-array-byte-offset: 1.0.0 - typed-array-length: 1.0.4 - unbox-primitive: 1.0.2 - which-typed-array: 1.1.13 - dev: true - /es-iterator-helpers@1.0.15: - resolution: {integrity: sha512-GhoY8uYqd6iwUl2kgjTm4CZAf6oo5mHK7BPqx3rKgx893YSsy0LGHV6gfqqQvZt/8xM8xeOnfXBCfqclMKkJ5g==} + /has-tostringtag@1.0.0: + resolution: {integrity: sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==} + engines: {node: '>= 0.4'} dependencies: - asynciterator.prototype: 1.0.0 - call-bind: 1.0.5 - define-properties: 1.2.1 - es-abstract: 1.22.3 - es-set-tostringtag: 2.0.2 - function-bind: 1.1.2 - get-intrinsic: 1.2.2 - globalthis: 1.0.3 - has-property-descriptors: 1.0.1 - has-proto: 1.0.1 has-symbols: 1.0.3 - internal-slot: 1.0.6 - iterator.prototype: 1.1.2 - safe-array-concat: 1.0.1 - dev: true - /es-set-tostringtag@2.0.2: - resolution: {integrity: sha512-BuDyupZt65P9D2D2vA/zqcI3G5xRsklm5N3xCwuiy+/vKy8i0ifdsQP1sLgO4tZDSCaQUSnmC48khknGMV3D2Q==} + /hasown@2.0.0: + resolution: {integrity: sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==} engines: {node: '>= 0.4'} dependencies: - get-intrinsic: 1.2.2 - has-tostringtag: 1.0.0 - hasown: 2.0.0 - dev: true + function-bind: 1.1.2 - /es-shim-unscopables@1.0.2: - resolution: {integrity: sha512-J3yBRXCzDu4ULnQwxyToo/OjdMx6akgVC7K6few0a7F/0wLtmKKN7I73AH5T2836UuXRqN7Qg+IIUw/+YJksRw==} - dependencies: - hasown: 2.0.0 - dev: true + /hexoid@1.0.0: + resolution: {integrity: sha512-QFLV0taWQOZtvIRIAdBChesmogZrtuXvVWsFHZTk2SU+anspqZ2vMnoLg7IE1+Uk16N19APic1BuF8bC8c2m5g==} + engines: {node: '>=8'} + dev: false - /es-to-primitive@1.2.1: - resolution: {integrity: sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==} - engines: {node: '>= 0.4'} + /hoist-non-react-statics@3.3.2: + resolution: {integrity: sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==} dependencies: - is-callable: 1.2.7 - is-date-object: 1.0.5 - is-symbol: 1.0.4 - dev: true + react-is: 16.13.1 + dev: false - /escalade@3.1.1: - resolution: {integrity: sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==} - engines: {node: '>=6'} - dev: true + /hook-std@3.0.0: + resolution: {integrity: sha512-jHRQzjSDzMtFy34AGj1DN+vq54WVuhSvKgrHf0OMiFQTwDD4L/qqofVEWjLOBMTn5+lCD3fPg32W9yOfnEJTTw==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dev: false - /escape-string-regexp@1.0.5: - resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} - engines: {node: '>=0.8.0'} - dev: true + /hosted-git-info@2.8.9: + resolution: {integrity: sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==} - /escape-string-regexp@4.0.0: - resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + /hosted-git-info@4.1.0: + resolution: {integrity: sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==} engines: {node: '>=10'} - dev: true - - /eslint-config-prettier@8.8.0(eslint@8.56.0): - resolution: {integrity: sha512-wLbQiFre3tdGgpDv67NQKnJuTlcUVYHas3k+DZCc2U2BadthoEY4B7hLPvAxaqdyOGCzuLfii2fqGph10va7oA==} - hasBin: true - peerDependencies: - eslint: '>=7.0.0' - dependencies: - eslint: 8.56.0 - dev: true - - /eslint-config-turbo@1.11.2(eslint@8.56.0): - resolution: {integrity: sha512-vqbyCH6kCHFoIAWUmGL61c0BfUQNz0XAl2RzAnEkSQ+PLXvEvuV2HsvL51UOzyyElfJlzZuh9T4BvUqb5KR9Eg==} - peerDependencies: - eslint: '>6.6.0' - dependencies: - eslint: 8.56.0 - eslint-plugin-turbo: 1.11.2(eslint@8.56.0) - dev: true + dependencies: + lru-cache: 6.0.0 + dev: false - /eslint-import-resolver-alias@1.1.2(eslint-plugin-import@2.29.1): - resolution: {integrity: sha512-WdviM1Eu834zsfjHtcGHtGfcu+F30Od3V7I9Fi57uhBEwPkjDcii7/yW8jAT+gOhn4P/vOxxNAXbFAKsrrc15w==} - engines: {node: '>= 4'} - peerDependencies: - eslint-plugin-import: '>=1.4.0' + /hosted-git-info@7.0.1: + resolution: {integrity: sha512-+K84LB1DYwMHoHSgaOY/Jfhw3ucPmSET5v98Ke/HdNSw4a0UktWzyW1mjhjpuxxTqOOsfWT/7iVshHmVZ4IpOA==} + engines: {node: ^16.14.0 || >=18.0.0} dependencies: - eslint-plugin-import: 2.29.1(@typescript-eslint/parser@6.1.0)(eslint@8.56.0) - dev: true + lru-cache: 10.1.0 + dev: false - /eslint-import-resolver-node@0.3.9: - resolution: {integrity: sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==} + /html-escaper@2.0.2: + resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} + + /http-cache-semantics@4.1.1: + resolution: {integrity: sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==} + dev: false + + /http-errors@2.0.0: + resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==} + engines: {node: '>= 0.8'} dependencies: - debug: 3.2.7 - is-core-module: 2.13.1 - resolve: 1.22.8 - transitivePeerDependencies: - - supports-color - dev: true + depd: 2.0.0 + inherits: 2.0.4 + setprototypeof: 1.2.0 + statuses: 2.0.1 + toidentifier: 1.0.1 + dev: false - /eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@5.62.0)(eslint-plugin-import@2.29.1)(eslint@8.56.0): - resolution: {integrity: sha512-xgdptdoi5W3niYeuQxKmzVDTATvLYqhpwmykwsh7f6HIOStGWEIL9iqZgQDF9u9OEzrRwR8no5q2VT+bjAujTg==} - engines: {node: ^14.18.0 || >=16.0.0} - peerDependencies: - eslint: '*' - eslint-plugin-import: '*' + /http-proxy-agent@7.0.0: + resolution: {integrity: sha512-+ZT+iBxVUQ1asugqnD6oWoRiS25AkjNfG085dKJGtGxkdwLQrMKU5wJr2bOOFAXzKcTuqq+7fZlTMgG3SRfIYQ==} + engines: {node: '>= 14'} dependencies: + agent-base: 7.1.0 debug: 4.3.4 - enhanced-resolve: 5.15.0 - eslint: 8.56.0 - eslint-module-utils: 2.8.0(@typescript-eslint/parser@5.62.0)(eslint-import-resolver-typescript@3.6.1)(eslint@8.56.0) - eslint-plugin-import: 2.29.1(@typescript-eslint/parser@6.1.0)(eslint@8.56.0) - fast-glob: 3.3.2 - get-tsconfig: 4.7.2 - is-core-module: 2.13.1 - is-glob: 4.0.3 transitivePeerDependencies: - - '@typescript-eslint/parser' - - eslint-import-resolver-node - - eslint-import-resolver-webpack - supports-color - dev: true + dev: false - /eslint-module-utils@2.8.0(@typescript-eslint/parser@5.62.0)(eslint-import-resolver-typescript@3.6.1)(eslint@8.56.0): - resolution: {integrity: sha512-aWajIYfsqCKRDgUfjEXNN/JlrzauMuSEy5sbd7WXbtW3EH6A6MpwEh42c7qD+MqQo9QMJ6fWLAeIJynx0g6OAw==} - engines: {node: '>=4'} - peerDependencies: - '@typescript-eslint/parser': '*' - eslint: '*' - eslint-import-resolver-node: '*' - eslint-import-resolver-typescript: '*' - eslint-import-resolver-webpack: '*' - peerDependenciesMeta: - '@typescript-eslint/parser': - optional: true - eslint: - optional: true - eslint-import-resolver-node: - optional: true - eslint-import-resolver-typescript: - optional: true - eslint-import-resolver-webpack: - optional: true + /http2-wrapper@1.0.3: + resolution: {integrity: sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==} + engines: {node: '>=10.19.0'} dependencies: - '@typescript-eslint/parser': 5.62.0(eslint@8.56.0)(typescript@5.3.3) - debug: 3.2.7 - eslint: 8.56.0 - eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@5.62.0)(eslint-plugin-import@2.29.1)(eslint@8.56.0) - transitivePeerDependencies: - - supports-color - dev: true + quick-lru: 5.1.1 + resolve-alpn: 1.2.1 + dev: false - /eslint-module-utils@2.8.0(@typescript-eslint/parser@6.1.0)(eslint-import-resolver-node@0.3.9)(eslint@8.56.0): - resolution: {integrity: sha512-aWajIYfsqCKRDgUfjEXNN/JlrzauMuSEy5sbd7WXbtW3EH6A6MpwEh42c7qD+MqQo9QMJ6fWLAeIJynx0g6OAw==} - engines: {node: '>=4'} - peerDependencies: - '@typescript-eslint/parser': '*' - eslint: '*' - eslint-import-resolver-node: '*' - eslint-import-resolver-typescript: '*' - eslint-import-resolver-webpack: '*' - peerDependenciesMeta: - '@typescript-eslint/parser': - optional: true - eslint: - optional: true - eslint-import-resolver-node: - optional: true - eslint-import-resolver-typescript: - optional: true - eslint-import-resolver-webpack: - optional: true + /https-proxy-agent@5.0.1: + resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==} + engines: {node: '>= 6'} dependencies: - '@typescript-eslint/parser': 6.1.0(eslint@8.56.0)(typescript@5.3.3) - debug: 3.2.7 - eslint: 8.56.0 - eslint-import-resolver-node: 0.3.9 + agent-base: 6.0.2 + debug: 4.3.4 transitivePeerDependencies: - supports-color - dev: true - - /eslint-plugin-eslint-comments@3.2.0(eslint@8.56.0): - resolution: {integrity: sha512-0jkOl0hfojIHHmEHgmNdqv4fmh7300NdpA9FFpF7zaoLvB/QeXOGNLIo86oAveJFrfB1p05kC8hpEMHM8DwWVQ==} - engines: {node: '>=6.5.0'} - peerDependencies: - eslint: '>=4.19.1' - dependencies: - escape-string-regexp: 1.0.5 - eslint: 8.56.0 - ignore: 5.3.0 - dev: true + dev: false - /eslint-plugin-import@2.29.1(@typescript-eslint/parser@6.1.0)(eslint@8.56.0): - resolution: {integrity: sha512-BbPC0cuExzhiMo4Ff1BTVwHpjjv28C5R+btTOGaCRC7UEz801up0JadwkeSk5Ued6TG34uaczuVuH6qyy5YUxw==} - engines: {node: '>=4'} - peerDependencies: - '@typescript-eslint/parser': '*' - eslint: ^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 - peerDependenciesMeta: - '@typescript-eslint/parser': - optional: true + /https-proxy-agent@7.0.2: + resolution: {integrity: sha512-NmLNjm6ucYwtcUmL7JQC1ZQ57LmHP4lT15FQ8D61nak1rO6DH+fz5qNK2Ap5UN4ZapYICE3/0KodcLYSPsPbaA==} + engines: {node: '>= 14'} dependencies: - '@typescript-eslint/parser': 6.1.0(eslint@8.56.0)(typescript@5.3.3) - array-includes: 3.1.7 - array.prototype.findlastindex: 1.2.3 - array.prototype.flat: 1.3.2 - array.prototype.flatmap: 1.3.2 - debug: 3.2.7 - doctrine: 2.1.0 - eslint: 8.56.0 - eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.8.0(@typescript-eslint/parser@6.1.0)(eslint-import-resolver-node@0.3.9)(eslint@8.56.0) - hasown: 2.0.0 - is-core-module: 2.13.1 - is-glob: 4.0.3 - minimatch: 3.1.2 - object.fromentries: 2.0.7 - object.groupby: 1.0.1 - object.values: 1.1.7 - semver: 6.3.1 - tsconfig-paths: 3.15.0 + agent-base: 7.1.0 + debug: 4.3.4 transitivePeerDependencies: - - eslint-import-resolver-typescript - - eslint-import-resolver-webpack - supports-color - dev: true + dev: false - /eslint-plugin-jest@27.6.0(@typescript-eslint/eslint-plugin@5.62.0)(eslint@8.56.0)(typescript@5.3.3): - resolution: {integrity: sha512-MTlusnnDMChbElsszJvrwD1dN3x6nZl//s4JD23BxB6MgR66TZlL064su24xEIS3VACfAoHV1vgyMgPw8nkdng==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - peerDependencies: - '@typescript-eslint/eslint-plugin': ^5.0.0 || ^6.0.0 - eslint: ^7.0.0 || ^8.0.0 - jest: '*' - peerDependenciesMeta: - '@typescript-eslint/eslint-plugin': - optional: true - jest: - optional: true + /human-signals@2.1.0: + resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} + engines: {node: '>=10.17.0'} + + /human-signals@4.3.1: + resolution: {integrity: sha512-nZXjEF2nbo7lIw3mgYjItAfgQXog3OjJogSbKa2CQIIvSGWcKgeJnQlNXip6NglNzYH45nSRiEVimMvYL8DDqQ==} + engines: {node: '>=14.18.0'} + dev: false + + /human-signals@5.0.0: + resolution: {integrity: sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==} + engines: {node: '>=16.17.0'} + dev: false + + /iconv-lite@0.4.24: + resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} + engines: {node: '>=0.10.0'} dependencies: - '@typescript-eslint/eslint-plugin': 5.62.0(@typescript-eslint/parser@5.62.0)(eslint@8.56.0)(typescript@5.3.3) - '@typescript-eslint/utils': 5.62.0(eslint@8.56.0)(typescript@5.3.3) - eslint: 8.56.0 - transitivePeerDependencies: - - supports-color - - typescript + safer-buffer: 2.1.2 + dev: false + + /ieee754@1.2.1: + resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + dev: false + + /ignore-by-default@1.0.1: + resolution: {integrity: sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==} dev: true - /eslint-plugin-jsx-a11y@6.8.0(eslint@8.56.0): - resolution: {integrity: sha512-Hdh937BS3KdwwbBaKd5+PLCOmYY6U4f2h9Z2ktwtNKvIdIEu137rjYbcb9ApSbVJfWxANNuiKTD/9tOKjK9qOA==} - engines: {node: '>=4.0'} - peerDependencies: - eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 + /ignore@5.3.0: + resolution: {integrity: sha512-g7dmpshy+gD7mh88OC9NwSGTKoc3kyLAZQRU1mt53Aw/vnvfXnbC+F/7F7QoYVKbV+KNvJx8wArewKy1vXMtlg==} + engines: {node: '>= 4'} + + /import-fresh@3.3.0: + resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==} + engines: {node: '>=6'} dependencies: - '@babel/runtime': 7.23.6 - aria-query: 5.3.0 - array-includes: 3.1.7 - array.prototype.flatmap: 1.3.2 - ast-types-flow: 0.0.8 - axe-core: 4.7.0 - axobject-query: 3.2.1 - damerau-levenshtein: 1.0.8 - emoji-regex: 9.2.2 - es-iterator-helpers: 1.0.15 - eslint: 8.56.0 - hasown: 2.0.0 - jsx-ast-utils: 3.3.5 - language-tags: 1.0.9 - minimatch: 3.1.2 - object.entries: 1.1.7 - object.fromentries: 2.0.7 - dev: true + parent-module: 1.0.1 + resolve-from: 4.0.0 - /eslint-plugin-playwright@0.11.2(eslint-plugin-jest@27.6.0)(eslint@8.56.0): - resolution: {integrity: sha512-uRLRLk7uTzc8NE6t4wBU8dijQwHvC66R/h7xwdM779jsJjMUtSmeaB8ayRkkpfwi+UU5BEfwvDANwmE+ccMVDw==} - peerDependencies: - eslint: '>=7' - eslint-plugin-jest: '>=24' - peerDependenciesMeta: - eslint-plugin-jest: - optional: true + /import-from@4.0.0: + resolution: {integrity: sha512-P9J71vT5nLlDeV8FHs5nNxaLbrpfAV5cF5srvbZfpwpcJoM/xZR3hiv+q+SAnuSmuGbXMWud063iIMx/V/EWZQ==} + engines: {node: '>=12.2'} + dev: false + + /import-local@3.1.0: + resolution: {integrity: sha512-ASB07uLtnDs1o6EHjKpX34BKYDSqnFerfTOJL2HvMqF70LnxpjkzDB8J44oT9pu4AMPkQwf8jl6szgvNd2tRIg==} + engines: {node: '>=8'} + hasBin: true dependencies: - eslint: 8.56.0 - eslint-plugin-jest: 27.6.0(@typescript-eslint/eslint-plugin@5.62.0)(eslint@8.56.0)(typescript@5.3.3) - dev: true + pkg-dir: 4.2.0 + resolve-cwd: 3.0.0 - /eslint-plugin-prettier@5.0.0(eslint-config-prettier@8.8.0)(eslint@8.56.0)(prettier@3.0.0): - resolution: {integrity: sha512-AgaZCVuYDXHUGxj/ZGu1u8H8CYgDY3iG6w5kUFw4AzMVXzB7VvbKgYR4nATIN+OvUrghMbiDLeimVjVY5ilq3w==} - engines: {node: ^14.18.0 || >=16.0.0} - peerDependencies: - '@types/eslint': '>=8.0.0' - eslint: '>=8.0.0' - eslint-config-prettier: '*' - prettier: '>=3.0.0' - peerDependenciesMeta: - '@types/eslint': - optional: true - eslint-config-prettier: - optional: true + /imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + + /indent-string@4.0.0: + resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} + engines: {node: '>=8'} + + /indent-string@5.0.0: + resolution: {integrity: sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg==} + engines: {node: '>=12'} + dev: false + + /inflight@1.0.6: + resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} dependencies: - eslint: 8.56.0 - eslint-config-prettier: 8.8.0(eslint@8.56.0) - prettier: 3.0.0 - prettier-linter-helpers: 1.0.0 - synckit: 0.8.8 - dev: true + once: 1.4.0 + wrappy: 1.0.2 - /eslint-plugin-react-hooks@4.6.0(eslint@8.56.0): - resolution: {integrity: sha512-oFc7Itz9Qxh2x4gNHStv3BqJq54ExXmfC+a1NjAta66IAN87Wu0R/QArgIS9qKzX3dXKPI9H5crl9QchNMY9+g==} + /inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + /ini@1.3.8: + resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} + dev: false + + /ink-select-input@4.2.2(ink@3.2.0)(react@17.0.2): + resolution: {integrity: sha512-E5AS2Vnd4CSzEa7Rm+hG47wxRQo1ASfh4msKxO7FHmn/ym+GKSSsFIfR+FonqjKNDPXYJClw8lM47RdN3Pi+nw==} + engines: {node: '>=10'} + peerDependencies: + ink: ^3.0.5 + react: ^16.5.2 || ^17.0.0 + dependencies: + arr-rotate: 1.0.0 + figures: 3.2.0 + ink: 3.2.0(react@17.0.2) + lodash.isequal: 4.5.0 + react: 17.0.2 + dev: false + + /ink-spinner@4.0.3(ink@3.2.0)(react@17.0.2): + resolution: {integrity: sha512-uJ4nbH00MM9fjTJ5xdw0zzvtXMkeGb0WV6dzSWvFv2/+ks6FIhpkt+Ge/eLdh0Ah6Vjw5pLMyNfoHQpRDRVFbQ==} engines: {node: '>=10'} peerDependencies: - eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 + ink: '>=3.0.5' + react: '>=16.8.2' dependencies: - eslint: 8.56.0 - dev: true + cli-spinners: 2.9.1 + ink: 3.2.0(react@17.0.2) + react: 17.0.2 + dev: false - /eslint-plugin-react@7.33.2(eslint@8.56.0): - resolution: {integrity: sha512-73QQMKALArI8/7xGLNI/3LylrEYrlKZSb5C9+q3OtOewTnMQi5cT+aE9E41sLCmli3I9PGGmD1yiZydyo4FEPw==} - engines: {node: '>=4'} + /ink-table@3.1.0(ink@3.2.0)(react@17.0.2): + resolution: {integrity: sha512-qxVb4DIaEaJryvF9uZGydnmP9Hkmas3DCKVpEcBYC0E4eJd3qNgNe+PZKuzgCERFe9LfAS1TNWxCr9+AU4v3YA==} peerDependencies: - eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 + ink: '>=3.0.0' + react: '>=16.8.0' dependencies: - array-includes: 3.1.7 - array.prototype.flatmap: 1.3.2 - array.prototype.tosorted: 1.1.2 - doctrine: 2.1.0 - es-iterator-helpers: 1.0.15 - eslint: 8.56.0 - estraverse: 5.3.0 - jsx-ast-utils: 3.3.5 - minimatch: 3.1.2 - object.entries: 1.1.7 - object.fromentries: 2.0.7 - object.hasown: 1.1.3 - object.values: 1.1.7 - prop-types: 15.8.1 - resolve: 2.0.0-next.5 - semver: 6.3.1 - string.prototype.matchall: 4.0.10 - dev: true + ink: 3.2.0(react@17.0.2) + object-hash: 2.2.0 + react: 17.0.2 + dev: false - /eslint-plugin-testing-library@5.11.1(eslint@8.56.0)(typescript@5.3.3): - resolution: {integrity: sha512-5eX9e1Kc2PqVRed3taaLnAAqPZGEX75C+M/rXzUAI3wIg/ZxzUm1OVAwfe/O+vE+6YXOLetSe9g5GKD2ecXipw==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0, npm: '>=6'} + /ink-testing-library@2.1.0: + resolution: {integrity: sha512-7TNlOjJlJXB33vG7yVa+MMO7hCjaC1bCn+zdpSjknWoLbOWMaFdKc7LJvqVkZ0rZv2+akhjXPrcR/dbxissjUw==} + engines: {node: '>=10'} peerDependencies: - eslint: ^7.5.0 || ^8.0.0 - dependencies: - '@typescript-eslint/utils': 5.62.0(eslint@8.56.0)(typescript@5.3.3) - eslint: 8.56.0 - transitivePeerDependencies: - - supports-color - - typescript - dev: true - - /eslint-plugin-tsdoc@0.2.17: - resolution: {integrity: sha512-xRmVi7Zx44lOBuYqG8vzTXuL6IdGOeF9nHX17bjJ8+VE6fsxpdGem0/SBTmAwgYMKYB1WBkqRJVQ+n8GK041pA==} - dependencies: - '@microsoft/tsdoc': 0.14.2 - '@microsoft/tsdoc-config': 0.16.2 - dev: true + '@types/react': '>=16.8.0' + peerDependenciesMeta: + '@types/react': + optional: true + dev: false - /eslint-plugin-turbo@1.11.2(eslint@8.56.0): - resolution: {integrity: sha512-U6DX+WvgGFiwEAqtOjm4Ejd9O4jsw8jlFNkQi0ywxbMnbiTie+exF4Z0F/B1ajtjjeZkBkgRnlU+UkoraBN+bw==} + /ink-use-stdout-dimensions@1.0.5(ink@3.2.0)(react@17.0.2): + resolution: {integrity: sha512-rVsqnw4tQEAJUoknU09+zHdDf30GJdkumkHr0iz/TOYMYEZJkYqziQSGJAM+Z+M603EDfO89+Nxyn/Ko2Zknfw==} peerDependencies: - eslint: '>6.6.0' + ink: '>=2.0.0' + react: '>=16.0.0' dependencies: - dotenv: 16.0.3 - eslint: 8.56.0 - dev: true + ink: 3.2.0(react@17.0.2) + react: 17.0.2 + dev: false - /eslint-plugin-unicorn@43.0.2(eslint@8.56.0): - resolution: {integrity: sha512-DtqZ5mf/GMlfWoz1abIjq5jZfaFuHzGBZYIeuJfEoKKGWRHr2JiJR+ea+BF7Wx2N1PPRoT/2fwgiK1NnmNE3Hg==} - engines: {node: '>=14.18'} + /ink@3.2.0(react@17.0.2): + resolution: {integrity: sha512-firNp1q3xxTzoItj/eOOSZQnYSlyrWks5llCTVX37nJ59K3eXbQ8PtzCguqo8YI19EELo5QxaKnJd4VxzhU8tg==} + engines: {node: '>=10'} peerDependencies: - eslint: '>=8.18.0' + '@types/react': '>=16.8.0' + react: '>=16.8.0' + peerDependenciesMeta: + '@types/react': + optional: true dependencies: - '@babel/helper-validator-identifier': 7.22.20 - ci-info: 3.9.0 - clean-regexp: 1.0.0 - eslint: 8.56.0 - eslint-utils: 3.0.0(eslint@8.56.0) - esquery: 1.5.0 + ansi-escapes: 4.3.2 + auto-bind: 4.0.0 + chalk: 4.1.2 + cli-boxes: 2.2.1 + cli-cursor: 3.1.0 + cli-truncate: 2.1.0 + code-excerpt: 3.0.0 indent-string: 4.0.0 - is-builtin-module: 3.2.1 + is-ci: 2.0.0 lodash: 4.17.21 - pluralize: 8.0.0 - read-pkg-up: 7.0.1 - regexp-tree: 0.1.27 - safe-regex: 2.1.1 - semver: 7.5.4 - strip-indent: 3.0.0 - dev: true + patch-console: 1.0.0 + react: 17.0.2 + react-devtools-core: 4.28.5 + react-reconciler: 0.26.2(react@17.0.2) + scheduler: 0.20.2 + signal-exit: 3.0.7 + slice-ansi: 3.0.0 + stack-utils: 2.0.6 + string-width: 4.2.3 + type-fest: 0.12.0 + widest-line: 3.1.0 + wrap-ansi: 6.2.0 + ws: 7.5.9 + yoga-layout-prebuilt: 1.10.0 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + dev: false - /eslint-scope@5.1.1: - resolution: {integrity: sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==} - engines: {node: '>=8.0.0'} + /internal-slot@1.0.6: + resolution: {integrity: sha512-Xj6dv+PsbtwyPpEflsejS+oIZxmMlV44zAhG479uYu89MsjcYOhCFnNyKrkJrihbsiasQyY0afoCl/9BLR65bg==} + engines: {node: '>= 0.4'} dependencies: - esrecurse: 4.3.0 - estraverse: 4.3.0 - dev: true + get-intrinsic: 1.2.2 + hasown: 2.0.0 + side-channel: 1.0.4 - /eslint-scope@7.2.2: - resolution: {integrity: sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - dependencies: - esrecurse: 4.3.0 - estraverse: 5.3.0 - dev: true + /interpret@1.4.0: + resolution: {integrity: sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==} + engines: {node: '>= 0.10'} + dev: false - /eslint-utils@3.0.0(eslint@8.56.0): - resolution: {integrity: sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==} - engines: {node: ^10.0.0 || ^12.0.0 || >= 14.0.0} - peerDependencies: - eslint: '>=5' + /into-stream@7.0.0: + resolution: {integrity: sha512-2dYz766i9HprMBasCMvHMuazJ7u4WzhJwo5kb3iPSiW/iRYV6uPari3zHoqZlnuaR7V1bEiNMxikhp37rdBXbw==} + engines: {node: '>=12'} dependencies: - eslint: 8.56.0 - eslint-visitor-keys: 2.1.0 - dev: true - - /eslint-visitor-keys@2.1.0: - resolution: {integrity: sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==} - engines: {node: '>=10'} - dev: true - - /eslint-visitor-keys@3.4.3: - resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - dev: true + from2: 2.3.0 + p-is-promise: 3.0.0 + dev: false - /eslint@8.56.0: - resolution: {integrity: sha512-Go19xM6T9puCOWntie1/P997aXxFsOi37JIHRWI514Hc6ZnaHGKY9xFhrU65RT6CcBEzZoGG1e6Nq+DT04ZtZQ==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - hasBin: true + /ioredis@4.28.5: + resolution: {integrity: sha512-3GYo0GJtLqgNXj4YhrisLaNNvWSNwSS2wS4OELGfGxH8I69+XfNdnmV1AyN+ZqMh0i7eX+SWjrwFKDBDgfBC1A==} + engines: {node: '>=6'} dependencies: - '@eslint-community/eslint-utils': 4.4.0(eslint@8.56.0) - '@eslint-community/regexpp': 4.10.0 - '@eslint/eslintrc': 2.1.4 - '@eslint/js': 8.56.0 - '@humanwhocodes/config-array': 0.11.13 - '@humanwhocodes/module-importer': 1.0.1 - '@nodelib/fs.walk': 1.2.8 - '@ungap/structured-clone': 1.2.0 - ajv: 6.12.6 - chalk: 4.1.2 - cross-spawn: 7.0.3 + cluster-key-slot: 1.1.2 debug: 4.3.4 - doctrine: 3.0.0 - escape-string-regexp: 4.0.0 - eslint-scope: 7.2.2 - eslint-visitor-keys: 3.4.3 - espree: 9.6.1 - esquery: 1.5.0 - esutils: 2.0.3 - fast-deep-equal: 3.1.3 - file-entry-cache: 6.0.1 - find-up: 5.0.0 - glob-parent: 6.0.2 - globals: 13.24.0 - graphemer: 1.4.0 - ignore: 5.3.0 - imurmurhash: 0.1.4 - is-glob: 4.0.3 - is-path-inside: 3.0.3 - js-yaml: 4.1.0 - json-stable-stringify-without-jsonify: 1.0.1 - levn: 0.4.1 - lodash.merge: 4.6.2 - minimatch: 3.1.2 - natural-compare: 1.4.0 - optionator: 0.9.3 - strip-ansi: 6.0.1 - text-table: 0.2.0 + denque: 1.5.1 + lodash.defaults: 4.2.0 + lodash.flatten: 4.4.0 + lodash.isarguments: 3.1.0 + p-map: 2.1.0 + redis-commands: 1.7.0 + redis-errors: 1.2.0 + redis-parser: 3.0.0 + standard-as-callback: 2.1.0 transitivePeerDependencies: - supports-color - dev: true + dev: false - /espree@9.6.1: - resolution: {integrity: sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - dependencies: - acorn: 8.11.2 - acorn-jsx: 5.3.2(acorn@8.11.2) - eslint-visitor-keys: 3.4.3 - dev: true + /ip-regex@4.3.0: + resolution: {integrity: sha512-B9ZWJxHHOHUhUjCPrMpLD4xEq35bUTClHM1S6CBU5ixQnkZmwipwgc96vAd7AAGM9TGHvJR+Uss+/Ak6UphK+Q==} + engines: {node: '>=8'} + dev: false - /esquery@1.5.0: - resolution: {integrity: sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==} - engines: {node: '>=0.10'} + /ipaddr.js@1.9.1: + resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} + engines: {node: '>= 0.10'} + dev: false + + /is-arguments@1.1.1: + resolution: {integrity: sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==} + engines: {node: '>= 0.4'} dependencies: - estraverse: 5.3.0 - dev: true + call-bind: 1.0.5 + has-tostringtag: 1.0.0 + dev: false - /esrecurse@4.3.0: - resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} - engines: {node: '>=4.0'} + /is-array-buffer@3.0.2: + resolution: {integrity: sha512-y+FyyR/w8vfIRq4eQcM1EYgSTnmHXPqaF+IgzgraytCFq5Xh8lllDVmAZolPJiZttZLeFSINPYMaEJ7/vWUa1w==} dependencies: - estraverse: 5.3.0 - dev: true + call-bind: 1.0.5 + get-intrinsic: 1.2.2 + is-typed-array: 1.1.12 - /estraverse@4.3.0: - resolution: {integrity: sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==} - engines: {node: '>=4.0'} - dev: true + /is-arrayish@0.2.1: + resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} - /estraverse@5.3.0: - resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} - engines: {node: '>=4.0'} - dev: true + /is-arrayish@0.3.2: + resolution: {integrity: sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==} + dev: false - /esutils@2.0.3: - resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} - engines: {node: '>=0.10.0'} + /is-async-function@2.0.0: + resolution: {integrity: sha512-Y1JXKrfykRJGdlDwdKlLpLyMIiWqWvuSd17TvZk68PLAOGOoF4Xyav1z0Xhoi+gCYjZVeC5SI+hYFOfvXmGRCA==} + engines: {node: '>= 0.4'} + dependencies: + has-tostringtag: 1.0.0 dev: true - /fast-deep-equal@3.1.3: - resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} - dev: true + /is-bigint@1.0.4: + resolution: {integrity: sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==} + dependencies: + has-bigints: 1.0.2 - /fast-diff@1.3.0: - resolution: {integrity: sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==} - dev: true + /is-binary-path@2.1.0: + resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} + engines: {node: '>=8'} + dependencies: + binary-extensions: 2.2.0 - /fast-glob@3.3.2: - resolution: {integrity: sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==} - engines: {node: '>=8.6.0'} + /is-boolean-object@1.1.2: + resolution: {integrity: sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==} + engines: {node: '>= 0.4'} dependencies: - '@nodelib/fs.stat': 2.0.5 - '@nodelib/fs.walk': 1.2.8 - glob-parent: 5.1.2 - merge2: 1.4.1 - micromatch: 4.0.5 - dev: true + call-bind: 1.0.5 + has-tostringtag: 1.0.0 - /fast-json-stable-stringify@2.1.0: - resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} - dev: true + /is-buffer@1.1.6: + resolution: {integrity: sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==} + dev: false - /fast-levenshtein@2.0.6: - resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + /is-builtin-module@3.2.1: + resolution: {integrity: sha512-BSLE3HnV2syZ0FK0iMA/yUGplUeMmNz4AW5fnTunbCIqZi4vG3WjJT9FHMy5D69xmAYBHXQhJdALdpwVxV501A==} + engines: {node: '>=6'} + dependencies: + builtin-modules: 3.3.0 dev: true - /fastq@1.16.0: - resolution: {integrity: sha512-ifCoaXsDrsdkWTtiNJX5uzHDsrck5TzfKKDcuFFTIrrc/BS076qgEIfoIy1VeZqViznfKiysPYTh/QeHtnIsYA==} + /is-callable@1.2.7: + resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} + engines: {node: '>= 0.4'} + + /is-ci@2.0.0: + resolution: {integrity: sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w==} + hasBin: true + dependencies: + ci-info: 2.0.0 + dev: false + + /is-core-module@2.13.1: + resolution: {integrity: sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==} dependencies: - reusify: 1.0.4 - dev: true + hasown: 2.0.0 - /file-entry-cache@6.0.1: - resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==} - engines: {node: ^10.12.0 || >=12.0.0} + /is-date-object@1.0.5: + resolution: {integrity: sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==} + engines: {node: '>= 0.4'} dependencies: - flat-cache: 3.2.0 - dev: true + has-tostringtag: 1.0.0 - /fill-range@7.0.1: - resolution: {integrity: sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==} + /is-docker@2.2.1: + resolution: {integrity: sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==} engines: {node: '>=8'} + hasBin: true + dev: false + + /is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + /is-finalizationregistry@1.0.2: + resolution: {integrity: sha512-0by5vtUJs8iFQb5TYUHHPudOR+qXYIMKtiUzvLIZITZUjknFmziyBJuLhVRc+Ds0dREFlskDNJKYIdIzu/9pfw==} dependencies: - to-regex-range: 5.0.1 + call-bind: 1.0.5 dev: true - /find-up@4.1.0: - resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} + /is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} engines: {node: '>=8'} + + /is-generator-fn@2.1.0: + resolution: {integrity: sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==} + engines: {node: '>=6'} + + /is-generator-function@1.0.10: + resolution: {integrity: sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==} + engines: {node: '>= 0.4'} dependencies: - locate-path: 5.0.0 - path-exists: 4.0.0 + has-tostringtag: 1.0.0 dev: true - /find-up@5.0.0: - resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} - engines: {node: '>=10'} + /is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} dependencies: - locate-path: 6.0.0 - path-exists: 4.0.0 - dev: true + is-extglob: 2.1.1 - /flat-cache@3.2.0: - resolution: {integrity: sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==} - engines: {node: ^10.12.0 || >=12.0.0} + /is-ip@3.1.0: + resolution: {integrity: sha512-35vd5necO7IitFPjd/YBeqwWnyDWbuLH9ZXQdMfDA8TEo7pv5X8yfrvVO3xbJbLUlERCMvf6X0hTUamQxCYJ9Q==} + engines: {node: '>=8'} dependencies: - flatted: 3.2.9 - keyv: 4.5.4 - rimraf: 3.0.2 - dev: true + ip-regex: 4.3.0 + dev: false - /flatted@3.2.9: - resolution: {integrity: sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==} + /is-map@2.0.2: + resolution: {integrity: sha512-cOZFQQozTha1f4MxLFzlgKYPTyj26picdZTx82hbc/Xf4K/tZOOXSCkMvU4pKioRXGDLJRn0GM7Upe7kR721yg==} + + /is-negative-zero@2.0.2: + resolution: {integrity: sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==} + engines: {node: '>= 0.4'} dev: true - /for-each@0.3.3: - resolution: {integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==} + /is-number-object@1.0.7: + resolution: {integrity: sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==} + engines: {node: '>= 0.4'} dependencies: - is-callable: 1.2.7 - dev: true + has-tostringtag: 1.0.0 - /fs.realpath@1.0.0: - resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} - dev: true + /is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} - /function-bind@1.1.2: - resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + /is-obj@2.0.0: + resolution: {integrity: sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==} + engines: {node: '>=8'} + dev: false + + /is-path-inside@3.0.3: + resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==} + engines: {node: '>=8'} + + /is-plain-obj@1.1.0: + resolution: {integrity: sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==} + engines: {node: '>=0.10.0'} + dev: false + + /is-plain-obj@4.1.0: + resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} + engines: {node: '>=12'} dev: true - /function.prototype.name@1.1.6: - resolution: {integrity: sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg==} + /is-regex@1.1.4: + resolution: {integrity: sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==} engines: {node: '>= 0.4'} dependencies: call-bind: 1.0.5 - define-properties: 1.2.1 - es-abstract: 1.22.3 - functions-have-names: 1.2.3 - dev: true + has-tostringtag: 1.0.0 - /functions-have-names@1.2.3: - resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} - dev: true + /is-set@2.0.2: + resolution: {integrity: sha512-+2cnTEZeY5z/iXGbLhPrOAaK/Mau5k5eXq9j14CpRTftq0pAJu2MwVRSZhyZWBzx3o6X795Lz6Bpb6R0GKf37g==} - /gensync@1.0.0-beta.2: - resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} - engines: {node: '>=6.9.0'} - dev: true + /is-shared-array-buffer@1.0.2: + resolution: {integrity: sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==} + dependencies: + call-bind: 1.0.5 - /get-intrinsic@1.2.2: - resolution: {integrity: sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA==} + /is-stream@2.0.1: + resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} + engines: {node: '>=8'} + + /is-stream@3.0.0: + resolution: {integrity: sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dev: false + + /is-string@1.0.7: + resolution: {integrity: sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==} + engines: {node: '>= 0.4'} + dependencies: + has-tostringtag: 1.0.0 + + /is-symbol@1.0.4: + resolution: {integrity: sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==} + engines: {node: '>= 0.4'} dependencies: - function-bind: 1.1.2 - has-proto: 1.0.1 has-symbols: 1.0.3 - hasown: 2.0.0 - dev: true - /get-stdin@9.0.0: - resolution: {integrity: sha512-dVKBjfWisLAicarI2Sf+JuBE/DghV4UzNAVe9yhEJuzeREd3JhOTE9cUaJTeSa77fsbQUK3pcOpJfM59+VKZaA==} - engines: {node: '>=12'} - dev: true + /is-text-path@2.0.0: + resolution: {integrity: sha512-+oDTluR6WEjdXEJMnC2z6A4FRwFoYuvShVVEGsS7ewc0UTi2QtAKMDJuL4BDEVt+5T7MjFo12RP8ghOM75oKJw==} + engines: {node: '>=8'} + dependencies: + text-extensions: 2.4.0 + dev: false - /get-symbol-description@1.0.0: - resolution: {integrity: sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==} + /is-typed-array@1.1.12: + resolution: {integrity: sha512-Z14TF2JNG8Lss5/HMqt0//T9JeHXttXy5pH/DBU4vi98ozO2btxzq9MwYDZYnKwU8nRsz/+GVFVRDq3DkVuSPg==} engines: {node: '>= 0.4'} dependencies: - call-bind: 1.0.5 - get-intrinsic: 1.2.2 - dev: true + which-typed-array: 1.1.13 - /get-tsconfig@4.7.2: - resolution: {integrity: sha512-wuMsz4leaj5hbGgg4IvDU0bqJagpftG5l5cXIAvo8uZrqn0NJqwtfupTN00VnkQJPcIRrxYrm1Ue24btpCha2A==} + /is-unicode-supported@1.3.0: + resolution: {integrity: sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==} + engines: {node: '>=12'} + dev: false + + /is-valid-domain@0.1.6: + resolution: {integrity: sha512-ZKtq737eFkZr71At8NxOFcP9O1K89gW3DkdrGMpp1upr/ueWjj+Weh4l9AI4rN0Gt8W2M1w7jrG2b/Yv83Ljpg==} dependencies: - resolve-pkg-maps: 1.0.0 - dev: true + punycode: 2.3.1 + dev: false - /git-hooks-list@3.1.0: - resolution: {integrity: sha512-LF8VeHeR7v+wAbXqfgRlTSX/1BJR9Q1vEMR8JAz1cEg6GX07+zyj3sAdDvYjj/xnlIfVuGgj4qBei1K3hKH+PA==} - dev: true + /is-weakmap@2.0.1: + resolution: {integrity: sha512-NSBR4kH5oVj1Uwvv970ruUkCV7O1mzgVFO4/rev2cLRda9Tm9HrL70ZPut4rOHgY0FNrUu9BCbXA2sdQ+x0chA==} - /glob-parent@5.1.2: - resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} - engines: {node: '>= 6'} + /is-weakref@1.0.2: + resolution: {integrity: sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==} dependencies: - is-glob: 4.0.3 + call-bind: 1.0.5 dev: true - /glob-parent@6.0.2: - resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} - engines: {node: '>=10.13.0'} + /is-weakset@2.0.2: + resolution: {integrity: sha512-t2yVvttHkQktwnNNmBQ98AhENLdPUTDTE21uPqAQ0ARwQfGeQKRVS0NNurH7bTf7RrvcVn1OOge45CnBeHCSmg==} dependencies: - is-glob: 4.0.3 - dev: true + call-bind: 1.0.5 + get-intrinsic: 1.2.2 - /glob@7.2.3: - resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} + /is-wsl@2.2.0: + resolution: {integrity: sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==} + engines: {node: '>=8'} dependencies: - fs.realpath: 1.0.0 - inflight: 1.0.6 - inherits: 2.0.4 - minimatch: 3.1.2 - once: 1.4.0 - path-is-absolute: 1.0.1 - dev: true + is-docker: 2.2.1 + dev: false - /globals@11.12.0: - resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==} - engines: {node: '>=4'} + /isarray@0.0.1: + resolution: {integrity: sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==} dev: true - /globals@13.24.0: - resolution: {integrity: sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==} + /isarray@1.0.0: + resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} + dev: false + + /isarray@2.0.5: + resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} + + /isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + /issue-parser@6.0.0: + resolution: {integrity: sha512-zKa/Dxq2lGsBIXQ7CUZWTHfvxPC2ej0KfO7fIPqLlHB9J2hJ7rGhZ5rilhuufylr4RXYPzJUeFjKxz305OsNlA==} + engines: {node: '>=10.13'} + dependencies: + lodash.capitalize: 4.2.1 + lodash.escaperegexp: 4.1.2 + lodash.isplainobject: 4.0.6 + lodash.isstring: 4.0.1 + lodash.uniqby: 4.7.0 + dev: false + + /istanbul-lib-coverage@3.2.2: + resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==} + engines: {node: '>=8'} + + /istanbul-lib-instrument@5.2.1: + resolution: {integrity: sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==} engines: {node: '>=8'} dependencies: - type-fest: 0.20.2 - dev: true + '@babel/core': 7.23.6 + '@babel/parser': 7.23.6 + '@istanbuljs/schema': 0.1.3 + istanbul-lib-coverage: 3.2.2 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color - /globalthis@1.0.3: - resolution: {integrity: sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA==} - engines: {node: '>= 0.4'} + /istanbul-lib-instrument@6.0.1: + resolution: {integrity: sha512-EAMEJBsYuyyztxMxW3g7ugGPkrZsV57v0Hmv3mm1uQsmB+QnZuepg731CRaIgeUVSdmsTngOkSnauNF8p7FIhA==} + engines: {node: '>=10'} dependencies: - define-properties: 1.2.1 - dev: true + '@babel/core': 7.23.7 + '@babel/parser': 7.23.6 + '@istanbuljs/schema': 0.1.3 + istanbul-lib-coverage: 3.2.2 + semver: 7.5.4 + transitivePeerDependencies: + - supports-color - /globby@11.1.0: - resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} + /istanbul-lib-report@3.0.1: + resolution: {integrity: sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==} engines: {node: '>=10'} dependencies: - array-union: 2.1.0 - dir-glob: 3.0.1 - fast-glob: 3.3.2 - ignore: 5.3.0 - merge2: 1.4.1 - slash: 3.0.0 - dev: true + istanbul-lib-coverage: 3.2.2 + make-dir: 4.0.0 + supports-color: 7.2.0 - /globby@13.2.2: - resolution: {integrity: sha512-Y1zNGV+pzQdh7H39l9zgB4PJqjRNqydvdYCDG4HFXM4XuvSaQQlEc91IU1yALL8gUTDomgBAfz3XJdmUS+oo0w==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + /istanbul-lib-source-maps@4.0.1: + resolution: {integrity: sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==} + engines: {node: '>=10'} dependencies: - dir-glob: 3.0.1 - fast-glob: 3.3.2 - ignore: 5.3.0 - merge2: 1.4.1 - slash: 4.0.0 - dev: true + debug: 4.3.4 + istanbul-lib-coverage: 3.2.2 + source-map: 0.6.1 + transitivePeerDependencies: + - supports-color + + /istanbul-reports@3.1.6: + resolution: {integrity: sha512-TLgnMkKg3iTDsQ9PbPTdpfAK2DzjF9mqUG7RMgcQl8oFjad8ob4laGxv5XV5U9MAfx8D6tSJiUyuAwzLicaxlg==} + engines: {node: '>=8'} + dependencies: + html-escaper: 2.0.2 + istanbul-lib-report: 3.0.1 - /gopd@1.0.1: - resolution: {integrity: sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==} + /iterator.prototype@1.1.2: + resolution: {integrity: sha512-DR33HMMr8EzwuRL8Y9D3u2BMj8+RqSE850jfGu59kS7tbmPLzGkZmVSfyCFSDxuZiEY6Rzt3T2NA/qU+NwVj1w==} dependencies: + define-properties: 1.2.1 get-intrinsic: 1.2.2 + has-symbols: 1.0.3 + reflect.getprototypeof: 1.0.4 + set-function-name: 2.0.1 dev: true - /graceful-fs@4.2.11: - resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} - dev: true - - /graphemer@1.4.0: - resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} - dev: true + /jackspeak@2.3.6: + resolution: {integrity: sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==} + engines: {node: '>=14'} + dependencies: + '@isaacs/cliui': 8.0.2 + optionalDependencies: + '@pkgjs/parseargs': 0.11.0 + dev: false - /has-bigints@1.0.2: - resolution: {integrity: sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==} - dev: true + /java-properties@1.0.2: + resolution: {integrity: sha512-qjdpeo2yKlYTH7nFdK0vbZWuTCesk4o63v5iVOlhMQPfuIZQfW/HI35SjfhA+4qpg36rnFSvUK5b1m+ckIblQQ==} + engines: {node: '>= 0.6.0'} + dev: false - /has-flag@3.0.0: - resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} - engines: {node: '>=4'} + /jest-changed-files@28.1.3: + resolution: {integrity: sha512-esaOfUWJXk2nfZt9SPyC8gA1kNfdKLkQWyzsMlqq8msYSlNKfmZxfRgZn4Cd4MGVUF+7v6dBs0d5TOAKa7iIiA==} + engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} + dependencies: + execa: 5.1.1 + p-limit: 3.1.0 dev: true - /has-flag@4.0.0: - resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} - engines: {node: '>=8'} - dev: true + /jest-changed-files@29.7.0: + resolution: {integrity: sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + execa: 5.1.1 + jest-util: 29.7.0 + p-limit: 3.1.0 - /has-property-descriptors@1.0.1: - resolution: {integrity: sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==} + /jest-circus@28.1.3: + resolution: {integrity: sha512-cZ+eS5zc79MBwt+IhQhiEp0OeBddpc1n8MBo1nMB8A7oPMKEO+Sre+wHaLJexQUj9Ya/8NOBY0RESUgYjB6fow==} + engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} dependencies: - get-intrinsic: 1.2.2 + '@jest/environment': 28.1.3 + '@jest/expect': 28.1.3 + '@jest/test-result': 28.1.3 + '@jest/types': 28.1.3 + '@types/node': 20.10.8 + chalk: 4.1.2 + co: 4.6.0 + dedent: 0.7.0 + is-generator-fn: 2.1.0 + jest-each: 28.1.3 + jest-matcher-utils: 28.1.3 + jest-message-util: 28.1.3 + jest-runtime: 28.1.3 + jest-snapshot: 28.1.3 + jest-util: 28.1.3 + p-limit: 3.1.0 + pretty-format: 28.1.3 + slash: 3.0.0 + stack-utils: 2.0.6 + transitivePeerDependencies: + - supports-color dev: true - /has-proto@1.0.1: - resolution: {integrity: sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==} - engines: {node: '>= 0.4'} - dev: true + /jest-circus@29.7.0: + resolution: {integrity: sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/environment': 29.7.0 + '@jest/expect': 29.7.0 + '@jest/test-result': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 20.10.8 + chalk: 4.1.2 + co: 4.6.0 + dedent: 1.5.1 + is-generator-fn: 2.1.0 + jest-each: 29.7.0 + jest-matcher-utils: 29.7.0 + jest-message-util: 29.7.0 + jest-runtime: 29.7.0 + jest-snapshot: 29.7.0 + jest-util: 29.7.0 + p-limit: 3.1.0 + pretty-format: 29.7.0 + pure-rand: 6.0.4 + slash: 3.0.0 + stack-utils: 2.0.6 + transitivePeerDependencies: + - babel-plugin-macros + - supports-color - /has-symbols@1.0.3: - resolution: {integrity: sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==} - engines: {node: '>= 0.4'} + /jest-cli@28.1.3: + resolution: {integrity: sha512-roY3kvrv57Azn1yPgdTebPAXvdR2xfezaKKYzVxZ6It/5NCxzJym6tUI5P1zkdWhfUYkxEI9uZWcQdaFLo8mJQ==} + engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} + hasBin: true + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + dependencies: + '@jest/core': 28.1.3 + '@jest/test-result': 28.1.3 + '@jest/types': 28.1.3 + chalk: 4.1.2 + exit: 0.1.2 + graceful-fs: 4.2.11 + import-local: 3.1.0 + jest-config: 28.1.3(@types/node@20.10.8) + jest-util: 28.1.3 + jest-validate: 28.1.3 + prompts: 2.4.2 + yargs: 17.7.2 + transitivePeerDependencies: + - '@types/node' + - supports-color + - ts-node dev: true - /has-tostringtag@1.0.0: - resolution: {integrity: sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==} - engines: {node: '>= 0.4'} + /jest-cli@29.7.0(@types/node@20.10.8)(ts-node@10.9.2): + resolution: {integrity: sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + hasBin: true + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true dependencies: - has-symbols: 1.0.3 - dev: true + '@jest/core': 29.7.0(ts-node@10.9.2) + '@jest/test-result': 29.7.0 + '@jest/types': 29.6.3 + chalk: 4.1.2 + create-jest: 29.7.0(@types/node@20.10.8)(ts-node@10.9.2) + exit: 0.1.2 + import-local: 3.1.0 + jest-config: 29.7.0(@types/node@20.10.8)(ts-node@10.9.2) + jest-util: 29.7.0 + jest-validate: 29.7.0 + yargs: 17.7.2 + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - supports-color + - ts-node - /hasown@2.0.0: - resolution: {integrity: sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==} - engines: {node: '>= 0.4'} + /jest-config@28.1.3(@types/node@20.10.8): + resolution: {integrity: sha512-MG3INjByJ0J4AsNBm7T3hsuxKQqFIiRo/AUqb1q9LRKI5UU6Aar9JHbr9Ivn1TVwfUD9KirRoM/T6u8XlcQPHQ==} + engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} + peerDependencies: + '@types/node': '*' + ts-node: '>=9.0.0' + peerDependenciesMeta: + '@types/node': + optional: true + ts-node: + optional: true dependencies: - function-bind: 1.1.2 + '@babel/core': 7.23.6 + '@jest/test-sequencer': 28.1.3 + '@jest/types': 28.1.3 + '@types/node': 20.10.8 + babel-jest: 28.1.3(@babel/core@7.23.6) + chalk: 4.1.2 + ci-info: 3.9.0 + deepmerge: 4.3.1 + glob: 7.2.3 + graceful-fs: 4.2.11 + jest-circus: 28.1.3 + jest-environment-node: 28.1.3 + jest-get-type: 28.0.2 + jest-regex-util: 28.0.2 + jest-resolve: 28.1.3 + jest-runner: 28.1.3 + jest-util: 28.1.3 + jest-validate: 28.1.3 + micromatch: 4.0.5 + parse-json: 5.2.0 + pretty-format: 28.1.3 + slash: 3.0.0 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - supports-color dev: true - /hosted-git-info@2.8.9: - resolution: {integrity: sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==} - dev: true + /jest-config@29.7.0(@types/node@20.10.8)(ts-node@10.9.2): + resolution: {integrity: sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + '@types/node': '*' + ts-node: '>=9.0.0' + peerDependenciesMeta: + '@types/node': + optional: true + ts-node: + optional: true + dependencies: + '@babel/core': 7.23.7 + '@jest/test-sequencer': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 20.10.8 + babel-jest: 29.7.0(@babel/core@7.23.7) + chalk: 4.1.2 + ci-info: 3.9.0 + deepmerge: 4.3.1 + glob: 7.2.3 + graceful-fs: 4.2.11 + jest-circus: 29.7.0 + jest-environment-node: 29.7.0 + jest-get-type: 29.6.3 + jest-regex-util: 29.6.3 + jest-resolve: 29.7.0 + jest-runner: 29.7.0 + jest-util: 29.7.0 + jest-validate: 29.7.0 + micromatch: 4.0.5 + parse-json: 5.2.0 + pretty-format: 29.7.0 + slash: 3.0.0 + strip-json-comments: 3.1.1 + ts-node: 10.9.2(@types/node@20.10.8)(typescript@5.3.3) + transitivePeerDependencies: + - babel-plugin-macros + - supports-color - /ignore@5.3.0: - resolution: {integrity: sha512-g7dmpshy+gD7mh88OC9NwSGTKoc3kyLAZQRU1mt53Aw/vnvfXnbC+F/7F7QoYVKbV+KNvJx8wArewKy1vXMtlg==} - engines: {node: '>= 4'} + /jest-diff@28.1.3: + resolution: {integrity: sha512-8RqP1B/OXzjjTWkqMX67iqgwBVJRgCyKD3L9nq+6ZqJMdvjE8RgHktqZ6jNrkdMT+dJuYNI3rhQpxaz7drJHfw==} + engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} + dependencies: + chalk: 4.1.2 + diff-sequences: 28.1.1 + jest-get-type: 28.0.2 + pretty-format: 28.1.3 dev: true - /import-fresh@3.3.0: - resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==} - engines: {node: '>=6'} + /jest-diff@29.7.0: + resolution: {integrity: sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - parent-module: 1.0.1 - resolve-from: 4.0.0 - dev: true + chalk: 4.1.2 + diff-sequences: 29.6.3 + jest-get-type: 29.6.3 + pretty-format: 29.7.0 - /imurmurhash@0.1.4: - resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} - engines: {node: '>=0.8.19'} + /jest-docblock@28.1.1: + resolution: {integrity: sha512-3wayBVNiOYx0cwAbl9rwm5kKFP8yHH3d/fkEaL02NPTkDojPtheGB7HZSFY4wzX+DxyrvhXz0KSCVksmCknCuA==} + engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} + dependencies: + detect-newline: 3.1.0 dev: true - /indent-string@4.0.0: - resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} - engines: {node: '>=8'} - dev: true + /jest-docblock@29.7.0: + resolution: {integrity: sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + detect-newline: 3.1.0 - /inflight@1.0.6: - resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} + /jest-each@28.1.3: + resolution: {integrity: sha512-arT1z4sg2yABU5uogObVPvSlSMQlDA48owx07BDPAiasW0yYpYHYOo4HHLz9q0BVzDVU4hILFjzJw0So9aCL/g==} + engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} dependencies: - once: 1.4.0 - wrappy: 1.0.2 + '@jest/types': 28.1.3 + chalk: 4.1.2 + jest-get-type: 28.0.2 + jest-util: 28.1.3 + pretty-format: 28.1.3 dev: true - /inherits@2.0.4: - resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} - dev: true + /jest-each@29.7.0: + resolution: {integrity: sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/types': 29.6.3 + chalk: 4.1.2 + jest-get-type: 29.6.3 + jest-util: 29.7.0 + pretty-format: 29.7.0 - /internal-slot@1.0.6: - resolution: {integrity: sha512-Xj6dv+PsbtwyPpEflsejS+oIZxmMlV44zAhG479uYu89MsjcYOhCFnNyKrkJrihbsiasQyY0afoCl/9BLR65bg==} - engines: {node: '>= 0.4'} + /jest-environment-node@28.1.3: + resolution: {integrity: sha512-ugP6XOhEpjAEhGYvp5Xj989ns5cB1K6ZdjBYuS30umT4CQEETaxSiPcZ/E1kFktX4GkrcM4qu07IIlDYX1gp+A==} + engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} dependencies: - get-intrinsic: 1.2.2 - hasown: 2.0.0 - side-channel: 1.0.4 + '@jest/environment': 28.1.3 + '@jest/fake-timers': 28.1.3 + '@jest/types': 28.1.3 + '@types/node': 20.10.8 + jest-mock: 28.1.3 + jest-util: 28.1.3 dev: true - /is-array-buffer@3.0.2: - resolution: {integrity: sha512-y+FyyR/w8vfIRq4eQcM1EYgSTnmHXPqaF+IgzgraytCFq5Xh8lllDVmAZolPJiZttZLeFSINPYMaEJ7/vWUa1w==} + /jest-environment-node@29.7.0: + resolution: {integrity: sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - call-bind: 1.0.5 - get-intrinsic: 1.2.2 - is-typed-array: 1.1.12 - dev: true + '@jest/environment': 29.7.0 + '@jest/fake-timers': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 20.10.8 + jest-mock: 29.7.0 + jest-util: 29.7.0 - /is-arrayish@0.2.1: - resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} + /jest-get-type@28.0.2: + resolution: {integrity: sha512-ioj2w9/DxSYHfOm5lJKCdcAmPJzQXmbM/Url3rhlghrPvT3tt+7a/+oXc9azkKmLvoiXjtV83bEWqi+vs5nlPA==} + engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} dev: true - /is-async-function@2.0.0: - resolution: {integrity: sha512-Y1JXKrfykRJGdlDwdKlLpLyMIiWqWvuSd17TvZk68PLAOGOoF4Xyav1z0Xhoi+gCYjZVeC5SI+hYFOfvXmGRCA==} - engines: {node: '>= 0.4'} + /jest-get-type@29.6.3: + resolution: {integrity: sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + /jest-haste-map@28.1.3: + resolution: {integrity: sha512-3S+RQWDXccXDKSWnkHa/dPwt+2qwA8CJzR61w3FoYCvoo3Pn8tvGcysmMF0Bj0EX5RYvAI2EIvC57OmotfdtKA==} + engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} dependencies: - has-tostringtag: 1.0.0 + '@jest/types': 28.1.3 + '@types/graceful-fs': 4.1.9 + '@types/node': 20.10.8 + anymatch: 3.1.3 + fb-watchman: 2.0.2 + graceful-fs: 4.2.11 + jest-regex-util: 28.0.2 + jest-util: 28.1.3 + jest-worker: 28.1.3 + micromatch: 4.0.5 + walker: 1.0.8 + optionalDependencies: + fsevents: 2.3.3 dev: true - /is-bigint@1.0.4: - resolution: {integrity: sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==} + /jest-haste-map@29.7.0: + resolution: {integrity: sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - has-bigints: 1.0.2 - dev: true + '@jest/types': 29.6.3 + '@types/graceful-fs': 4.1.9 + '@types/node': 20.10.8 + anymatch: 3.1.3 + fb-watchman: 2.0.2 + graceful-fs: 4.2.11 + jest-regex-util: 29.6.3 + jest-util: 29.7.0 + jest-worker: 29.7.0 + micromatch: 4.0.5 + walker: 1.0.8 + optionalDependencies: + fsevents: 2.3.3 - /is-boolean-object@1.1.2: - resolution: {integrity: sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==} - engines: {node: '>= 0.4'} + /jest-leak-detector@28.1.3: + resolution: {integrity: sha512-WFVJhnQsiKtDEo5lG2mM0v40QWnBM+zMdHHyJs8AWZ7J0QZJS59MsyKeJHWhpBZBH32S48FOVvGyOFT1h0DlqA==} + engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} dependencies: - call-bind: 1.0.5 - has-tostringtag: 1.0.0 + jest-get-type: 28.0.2 + pretty-format: 28.1.3 dev: true - /is-builtin-module@3.2.1: - resolution: {integrity: sha512-BSLE3HnV2syZ0FK0iMA/yUGplUeMmNz4AW5fnTunbCIqZi4vG3WjJT9FHMy5D69xmAYBHXQhJdALdpwVxV501A==} - engines: {node: '>=6'} + /jest-leak-detector@29.7.0: + resolution: {integrity: sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - builtin-modules: 3.3.0 - dev: true + jest-get-type: 29.6.3 + pretty-format: 29.7.0 - /is-callable@1.2.7: - resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} - engines: {node: '>= 0.4'} + /jest-matcher-utils@28.1.3: + resolution: {integrity: sha512-kQeJ7qHemKfbzKoGjHHrRKH6atgxMk8Enkk2iPQ3XwO6oE/KYD8lMYOziCkeSB9G4adPM4nR1DE8Tf5JeWH6Bw==} + engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} + dependencies: + chalk: 4.1.2 + jest-diff: 28.1.3 + jest-get-type: 28.0.2 + pretty-format: 28.1.3 dev: true - /is-core-module@2.13.1: - resolution: {integrity: sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==} + /jest-matcher-utils@29.7.0: + resolution: {integrity: sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - hasown: 2.0.0 - dev: true + chalk: 4.1.2 + jest-diff: 29.7.0 + jest-get-type: 29.6.3 + pretty-format: 29.7.0 - /is-date-object@1.0.5: - resolution: {integrity: sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==} - engines: {node: '>= 0.4'} + /jest-message-util@28.1.3: + resolution: {integrity: sha512-PFdn9Iewbt575zKPf1286Ht9EPoJmYT7P0kY+RibeYZ2XtOr53pDLEFoTWXbd1h4JiGiWpTBC84fc8xMXQMb7g==} + engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} dependencies: - has-tostringtag: 1.0.0 + '@babel/code-frame': 7.23.5 + '@jest/types': 28.1.3 + '@types/stack-utils': 2.0.3 + chalk: 4.1.2 + graceful-fs: 4.2.11 + micromatch: 4.0.5 + pretty-format: 28.1.3 + slash: 3.0.0 + stack-utils: 2.0.6 dev: true - /is-extglob@2.1.1: - resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} - engines: {node: '>=0.10.0'} - dev: true + /jest-message-util@29.7.0: + resolution: {integrity: sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@babel/code-frame': 7.23.5 + '@jest/types': 29.6.3 + '@types/stack-utils': 2.0.3 + chalk: 4.1.2 + graceful-fs: 4.2.11 + micromatch: 4.0.5 + pretty-format: 29.7.0 + slash: 3.0.0 + stack-utils: 2.0.6 - /is-finalizationregistry@1.0.2: - resolution: {integrity: sha512-0by5vtUJs8iFQb5TYUHHPudOR+qXYIMKtiUzvLIZITZUjknFmziyBJuLhVRc+Ds0dREFlskDNJKYIdIzu/9pfw==} + /jest-mock@28.1.3: + resolution: {integrity: sha512-o3J2jr6dMMWYVH4Lh/NKmDXdosrsJgi4AviS8oXLujcjpCMBb1FMsblDnOXKZKfSiHLxYub1eS0IHuRXsio9eA==} + engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} dependencies: - call-bind: 1.0.5 + '@jest/types': 28.1.3 + '@types/node': 20.10.8 dev: true - /is-generator-function@1.0.10: - resolution: {integrity: sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==} - engines: {node: '>= 0.4'} + /jest-mock@29.7.0: + resolution: {integrity: sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - has-tostringtag: 1.0.0 - dev: true + '@jest/types': 29.6.3 + '@types/node': 20.10.8 + jest-util: 29.7.0 - /is-glob@4.0.3: - resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} - engines: {node: '>=0.10.0'} + /jest-pnp-resolver@1.2.3(jest-resolve@28.1.3): + resolution: {integrity: sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==} + engines: {node: '>=6'} + peerDependencies: + jest-resolve: '*' + peerDependenciesMeta: + jest-resolve: + optional: true dependencies: - is-extglob: 2.1.1 + jest-resolve: 28.1.3 dev: true - /is-map@2.0.2: - resolution: {integrity: sha512-cOZFQQozTha1f4MxLFzlgKYPTyj26picdZTx82hbc/Xf4K/tZOOXSCkMvU4pKioRXGDLJRn0GM7Upe7kR721yg==} - dev: true + /jest-pnp-resolver@1.2.3(jest-resolve@29.7.0): + resolution: {integrity: sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==} + engines: {node: '>=6'} + peerDependencies: + jest-resolve: '*' + peerDependenciesMeta: + jest-resolve: + optional: true + dependencies: + jest-resolve: 29.7.0 - /is-negative-zero@2.0.2: - resolution: {integrity: sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==} - engines: {node: '>= 0.4'} + /jest-regex-util@28.0.2: + resolution: {integrity: sha512-4s0IgyNIy0y9FK+cjoVYoxamT7Zeo7MhzqRGx7YDYmaQn1wucY9rotiGkBzzcMXTtjrCAP/f7f+E0F7+fxPNdw==} + engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} dev: true - /is-number-object@1.0.7: - resolution: {integrity: sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==} - engines: {node: '>= 0.4'} + /jest-regex-util@29.6.3: + resolution: {integrity: sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + /jest-resolve-dependencies@28.1.3: + resolution: {integrity: sha512-qa0QO2Q0XzQoNPouMbCc7Bvtsem8eQgVPNkwn9LnS+R2n8DaVDPL/U1gngC0LTl1RYXJU0uJa2BMC2DbTfFrHA==} + engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} dependencies: - has-tostringtag: 1.0.0 + jest-regex-util: 28.0.2 + jest-snapshot: 28.1.3 + transitivePeerDependencies: + - supports-color dev: true - /is-number@7.0.0: - resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} - engines: {node: '>=0.12.0'} - dev: true + /jest-resolve-dependencies@29.7.0: + resolution: {integrity: sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + jest-regex-util: 29.6.3 + jest-snapshot: 29.7.0 + transitivePeerDependencies: + - supports-color - /is-path-inside@3.0.3: - resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==} - engines: {node: '>=8'} + /jest-resolve@28.1.3: + resolution: {integrity: sha512-Z1W3tTjE6QaNI90qo/BJpfnvpxtaFTFw5CDgwpyE/Kz8U/06N1Hjf4ia9quUhCh39qIGWF1ZuxFiBiJQwSEYKQ==} + engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} + dependencies: + chalk: 4.1.2 + graceful-fs: 4.2.11 + jest-haste-map: 28.1.3 + jest-pnp-resolver: 1.2.3(jest-resolve@28.1.3) + jest-util: 28.1.3 + jest-validate: 28.1.3 + resolve: 1.22.8 + resolve.exports: 1.1.1 + slash: 3.0.0 dev: true - /is-plain-obj@4.1.0: - resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} - engines: {node: '>=12'} - dev: true + /jest-resolve@29.7.0: + resolution: {integrity: sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + chalk: 4.1.2 + graceful-fs: 4.2.11 + jest-haste-map: 29.7.0 + jest-pnp-resolver: 1.2.3(jest-resolve@29.7.0) + jest-util: 29.7.0 + jest-validate: 29.7.0 + resolve: 1.22.8 + resolve.exports: 2.0.2 + slash: 3.0.0 - /is-regex@1.1.4: - resolution: {integrity: sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==} - engines: {node: '>= 0.4'} + /jest-runner@28.1.3: + resolution: {integrity: sha512-GkMw4D/0USd62OVO0oEgjn23TM+YJa2U2Wu5zz9xsQB1MxWKDOlrnykPxnMsN0tnJllfLPinHTka61u0QhaxBA==} + engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} dependencies: - call-bind: 1.0.5 - has-tostringtag: 1.0.0 + '@jest/console': 28.1.3 + '@jest/environment': 28.1.3 + '@jest/test-result': 28.1.3 + '@jest/transform': 28.1.3 + '@jest/types': 28.1.3 + '@types/node': 20.10.8 + chalk: 4.1.2 + emittery: 0.10.2 + graceful-fs: 4.2.11 + jest-docblock: 28.1.1 + jest-environment-node: 28.1.3 + jest-haste-map: 28.1.3 + jest-leak-detector: 28.1.3 + jest-message-util: 28.1.3 + jest-resolve: 28.1.3 + jest-runtime: 28.1.3 + jest-util: 28.1.3 + jest-watcher: 28.1.3 + jest-worker: 28.1.3 + p-limit: 3.1.0 + source-map-support: 0.5.13 + transitivePeerDependencies: + - supports-color dev: true - /is-set@2.0.2: - resolution: {integrity: sha512-+2cnTEZeY5z/iXGbLhPrOAaK/Mau5k5eXq9j14CpRTftq0pAJu2MwVRSZhyZWBzx3o6X795Lz6Bpb6R0GKf37g==} + /jest-runner@29.7.0: + resolution: {integrity: sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/console': 29.7.0 + '@jest/environment': 29.7.0 + '@jest/test-result': 29.7.0 + '@jest/transform': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 20.10.8 + chalk: 4.1.2 + emittery: 0.13.1 + graceful-fs: 4.2.11 + jest-docblock: 29.7.0 + jest-environment-node: 29.7.0 + jest-haste-map: 29.7.0 + jest-leak-detector: 29.7.0 + jest-message-util: 29.7.0 + jest-resolve: 29.7.0 + jest-runtime: 29.7.0 + jest-util: 29.7.0 + jest-watcher: 29.7.0 + jest-worker: 29.7.0 + p-limit: 3.1.0 + source-map-support: 0.5.13 + transitivePeerDependencies: + - supports-color + + /jest-runtime@28.1.3: + resolution: {integrity: sha512-NU+881ScBQQLc1JHG5eJGU7Ui3kLKrmwCPPtYsJtBykixrM2OhVQlpMmFWJjMyDfdkGgBMNjXCGB/ebzsgNGQw==} + engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} + dependencies: + '@jest/environment': 28.1.3 + '@jest/fake-timers': 28.1.3 + '@jest/globals': 28.1.3 + '@jest/source-map': 28.1.2 + '@jest/test-result': 28.1.3 + '@jest/transform': 28.1.3 + '@jest/types': 28.1.3 + chalk: 4.1.2 + cjs-module-lexer: 1.2.3 + collect-v8-coverage: 1.0.2 + execa: 5.1.1 + glob: 7.2.3 + graceful-fs: 4.2.11 + jest-haste-map: 28.1.3 + jest-message-util: 28.1.3 + jest-mock: 28.1.3 + jest-regex-util: 28.0.2 + jest-resolve: 28.1.3 + jest-snapshot: 28.1.3 + jest-util: 28.1.3 + slash: 3.0.0 + strip-bom: 4.0.0 + transitivePeerDependencies: + - supports-color dev: true - /is-shared-array-buffer@1.0.2: - resolution: {integrity: sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==} + /jest-runtime@29.7.0: + resolution: {integrity: sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - call-bind: 1.0.5 - dev: true + '@jest/environment': 29.7.0 + '@jest/fake-timers': 29.7.0 + '@jest/globals': 29.7.0 + '@jest/source-map': 29.6.3 + '@jest/test-result': 29.7.0 + '@jest/transform': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 20.10.8 + chalk: 4.1.2 + cjs-module-lexer: 1.2.3 + collect-v8-coverage: 1.0.2 + glob: 7.2.3 + graceful-fs: 4.2.11 + jest-haste-map: 29.7.0 + jest-message-util: 29.7.0 + jest-mock: 29.7.0 + jest-regex-util: 29.6.3 + jest-resolve: 29.7.0 + jest-snapshot: 29.7.0 + jest-util: 29.7.0 + slash: 3.0.0 + strip-bom: 4.0.0 + transitivePeerDependencies: + - supports-color - /is-string@1.0.7: - resolution: {integrity: sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==} - engines: {node: '>= 0.4'} + /jest-snapshot@28.1.3: + resolution: {integrity: sha512-4lzMgtiNlc3DU/8lZfmqxN3AYD6GGLbl+72rdBpXvcV+whX7mDrREzkPdp2RnmfIiWBg1YbuFSkXduF2JcafJg==} + engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} dependencies: - has-tostringtag: 1.0.0 + '@babel/core': 7.23.6 + '@babel/generator': 7.23.6 + '@babel/plugin-syntax-typescript': 7.23.3(@babel/core@7.23.6) + '@babel/traverse': 7.23.6 + '@babel/types': 7.23.6 + '@jest/expect-utils': 28.1.3 + '@jest/transform': 28.1.3 + '@jest/types': 28.1.3 + '@types/babel__traverse': 7.20.4 + '@types/prettier': 2.7.3 + babel-preset-current-node-syntax: 1.0.1(@babel/core@7.23.6) + chalk: 4.1.2 + expect: 28.1.3 + graceful-fs: 4.2.11 + jest-diff: 28.1.3 + jest-get-type: 28.0.2 + jest-haste-map: 28.1.3 + jest-matcher-utils: 28.1.3 + jest-message-util: 28.1.3 + jest-util: 28.1.3 + natural-compare: 1.4.0 + pretty-format: 28.1.3 + semver: 7.5.4 + transitivePeerDependencies: + - supports-color dev: true - /is-symbol@1.0.4: - resolution: {integrity: sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==} - engines: {node: '>= 0.4'} + /jest-snapshot@29.7.0: + resolution: {integrity: sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - has-symbols: 1.0.3 - dev: true + '@babel/core': 7.23.7 + '@babel/generator': 7.23.6 + '@babel/plugin-syntax-jsx': 7.23.3(@babel/core@7.23.7) + '@babel/plugin-syntax-typescript': 7.23.3(@babel/core@7.23.7) + '@babel/types': 7.23.6 + '@jest/expect-utils': 29.7.0 + '@jest/transform': 29.7.0 + '@jest/types': 29.6.3 + babel-preset-current-node-syntax: 1.0.1(@babel/core@7.23.7) + chalk: 4.1.2 + expect: 29.7.0 + graceful-fs: 4.2.11 + jest-diff: 29.7.0 + jest-get-type: 29.6.3 + jest-matcher-utils: 29.7.0 + jest-message-util: 29.7.0 + jest-util: 29.7.0 + natural-compare: 1.4.0 + pretty-format: 29.7.0 + semver: 7.5.4 + transitivePeerDependencies: + - supports-color - /is-typed-array@1.1.12: - resolution: {integrity: sha512-Z14TF2JNG8Lss5/HMqt0//T9JeHXttXy5pH/DBU4vi98ozO2btxzq9MwYDZYnKwU8nRsz/+GVFVRDq3DkVuSPg==} - engines: {node: '>= 0.4'} + /jest-util@28.1.3: + resolution: {integrity: sha512-XdqfpHwpcSRko/C35uLYFM2emRAltIIKZiJ9eAmhjsj0CqZMa0p1ib0R5fWIqGhn1a103DebTbpqIaP1qCQ6tQ==} + engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} dependencies: - which-typed-array: 1.1.13 + '@jest/types': 28.1.3 + '@types/node': 20.10.8 + chalk: 4.1.2 + ci-info: 3.9.0 + graceful-fs: 4.2.11 + picomatch: 2.3.1 dev: true - /is-weakmap@2.0.1: - resolution: {integrity: sha512-NSBR4kH5oVj1Uwvv970ruUkCV7O1mzgVFO4/rev2cLRda9Tm9HrL70ZPut4rOHgY0FNrUu9BCbXA2sdQ+x0chA==} - dev: true + /jest-util@29.7.0: + resolution: {integrity: sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/types': 29.6.3 + '@types/node': 20.10.8 + chalk: 4.1.2 + ci-info: 3.9.0 + graceful-fs: 4.2.11 + picomatch: 2.3.1 - /is-weakref@1.0.2: - resolution: {integrity: sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==} + /jest-validate@28.1.3: + resolution: {integrity: sha512-SZbOGBWEsaTxBGCOpsRWlXlvNkvTkY0XxRfh7zYmvd8uL5Qzyg0CHAXiXKROflh801quA6+/DsT4ODDthOC/OA==} + engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} dependencies: - call-bind: 1.0.5 + '@jest/types': 28.1.3 + camelcase: 6.3.0 + chalk: 4.1.2 + jest-get-type: 28.0.2 + leven: 3.1.0 + pretty-format: 28.1.3 dev: true - /is-weakset@2.0.2: - resolution: {integrity: sha512-t2yVvttHkQktwnNNmBQ98AhENLdPUTDTE21uPqAQ0ARwQfGeQKRVS0NNurH7bTf7RrvcVn1OOge45CnBeHCSmg==} + /jest-validate@29.7.0: + resolution: {integrity: sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - call-bind: 1.0.5 - get-intrinsic: 1.2.2 + '@jest/types': 29.6.3 + camelcase: 6.3.0 + chalk: 4.1.2 + jest-get-type: 29.6.3 + leven: 3.1.0 + pretty-format: 29.7.0 + + /jest-watcher@28.1.3: + resolution: {integrity: sha512-t4qcqj9hze+jviFPUN3YAtAEeFnr/azITXQEMARf5cMwKY2SMBRnCQTXLixTl20OR6mLh9KLMrgVJgJISym+1g==} + engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} + dependencies: + '@jest/test-result': 28.1.3 + '@jest/types': 28.1.3 + '@types/node': 20.10.8 + ansi-escapes: 4.3.2 + chalk: 4.1.2 + emittery: 0.10.2 + jest-util: 28.1.3 + string-length: 4.0.2 dev: true - /isarray@2.0.5: - resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} + /jest-watcher@29.7.0: + resolution: {integrity: sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/test-result': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 20.10.8 + ansi-escapes: 4.3.2 + chalk: 4.1.2 + emittery: 0.13.1 + jest-util: 29.7.0 + string-length: 4.0.2 + + /jest-worker@28.1.3: + resolution: {integrity: sha512-CqRA220YV/6jCo8VWvAt1KKx6eek1VIHMPeLEbpcfSfkEeWyBNppynM/o6q+Wmw+sOhos2ml34wZbSX3G13//g==} + engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} + dependencies: + '@types/node': 20.10.8 + merge-stream: 2.0.0 + supports-color: 8.1.1 dev: true - /isexe@2.0.0: - resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + /jest-worker@29.7.0: + resolution: {integrity: sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@types/node': 20.10.8 + jest-util: 29.7.0 + merge-stream: 2.0.0 + supports-color: 8.1.1 - /iterator.prototype@1.1.2: - resolution: {integrity: sha512-DR33HMMr8EzwuRL8Y9D3u2BMj8+RqSE850jfGu59kS7tbmPLzGkZmVSfyCFSDxuZiEY6Rzt3T2NA/qU+NwVj1w==} + /jest@28.1.3: + resolution: {integrity: sha512-N4GT5on8UkZgH0O5LUavMRV1EDEhNTL0KEfRmDIeZHSV7p2XgLoY9t9VDUgL6o+yfdgYHVxuz81G8oB9VG5uyA==} + engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} + hasBin: true + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true dependencies: - define-properties: 1.2.1 - get-intrinsic: 1.2.2 - has-symbols: 1.0.3 - reflect.getprototypeof: 1.0.4 - set-function-name: 2.0.1 + '@jest/core': 28.1.3 + '@jest/types': 28.1.3 + import-local: 3.1.0 + jest-cli: 28.1.3 + transitivePeerDependencies: + - '@types/node' + - supports-color + - ts-node dev: true + /jest@29.7.0(@types/node@20.10.8)(ts-node@10.9.2): + resolution: {integrity: sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + hasBin: true + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + dependencies: + '@jest/core': 29.7.0(ts-node@10.9.2) + '@jest/types': 29.6.3 + import-local: 3.1.0 + jest-cli: 29.7.0(@types/node@20.10.8)(ts-node@10.9.2) + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - supports-color + - ts-node + /jju@1.4.0: resolution: {integrity: sha512-8wb9Yw966OSxApiCt0K3yNJL8pnNeIv+OEq2YMidz4FKP6nonSRoOXc80iXY4JaN2FC11B9qsNmDsm+ZOfMROA==} dev: true + /joycon@3.1.1: + resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==} + engines: {node: '>=10'} + dev: false + + /js-levenshtein@1.1.6: + resolution: {integrity: sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g==} + engines: {node: '>=0.10.0'} + dev: false + /js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} - dev: true + + /js-yaml@3.14.1: + resolution: {integrity: sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==} + hasBin: true + dependencies: + argparse: 1.0.10 + esprima: 4.0.1 /js-yaml@4.1.0: resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} hasBin: true dependencies: argparse: 2.0.1 - dev: true /jsesc@2.5.2: resolution: {integrity: sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==} engines: {node: '>=4'} hasBin: true - dev: true + + /jsii-pacmak@1.94.0: + resolution: {integrity: sha512-L5s3RZ0AOx1XfAhXsEjyeCteVrw6nwJLynL+t93eXVDcw7NFT7S0fCFXzQ4lpYQ23P/yVpSIy32J3zpUOf4uDQ==} + engines: {node: '>= 14.17.0'} + hasBin: true + dependencies: + '@jsii/check-node': 1.94.0 + '@jsii/spec': 1.94.0 + clone: 2.1.2 + codemaker: 1.94.0 + commonmark: 0.30.0 + escape-string-regexp: 4.0.0 + fs-extra: 10.1.0 + jsii-reflect: 1.94.0 + jsii-rosetta: 1.94.0 + semver: 7.5.4 + spdx-license-list: 6.8.0 + xmlbuilder: 15.1.1 + yargs: 16.2.0 + transitivePeerDependencies: + - supports-color + dev: false + + /jsii-reflect@1.94.0: + resolution: {integrity: sha512-Oupkl5iFFeq3GJ2a/fQNMnsXRMISmEKklPHksYs/l6MqrNFUQ5kg9oj1qxjSyaCpvvXBI8Eh7y73dqNE8w4cVw==} + engines: {node: '>= 14.17.0'} + hasBin: true + dependencies: + '@jsii/check-node': 1.94.0 + '@jsii/spec': 1.94.0 + chalk: 4.1.2 + fs-extra: 10.1.0 + oo-ascii-tree: 1.94.0 + yargs: 16.2.0 + dev: false + + /jsii-rosetta@1.94.0: + resolution: {integrity: sha512-FLQAxdZJsH0sg87S9u/e4+HDGr6Pth+UZ4ool3//MFMsw+C0iwagAlNVhZuyohMdlvumpQeg9Gr+FvoBZFoBrA==} + engines: {node: '>= 14.17.0'} + hasBin: true + dependencies: + '@jsii/check-node': 1.94.0 + '@jsii/spec': 1.94.0 + '@xmldom/xmldom': 0.8.10 + commonmark: 0.30.0 + fast-glob: 3.3.2 + jsii: 1.94.0 + semver: 7.5.4 + semver-intersect: 1.5.0 + stream-json: 1.8.0 + typescript: 3.9.10 + workerpool: 6.5.1 + yargs: 16.2.0 + transitivePeerDependencies: + - supports-color + dev: false + + /jsii-rosetta@5.3.3: + resolution: {integrity: sha512-p00zNkx1yicyAyWGWnpIXutryqx3Q1Hko3yPWrhOsSEqy/WiKGAKme87S/EZVnpz0TAse/mX4OymlBCqDXagJw==} + engines: {node: '>= 18.12.0'} + hasBin: true + dependencies: + '@jsii/check-node': 1.93.0 + '@jsii/spec': 1.94.0 + '@xmldom/xmldom': 0.8.10 + chalk: 4.1.2 + commonmark: 0.30.0 + fast-glob: 3.3.2 + jsii: 5.3.3 + semver: 7.5.4 + semver-intersect: 1.5.0 + stream-json: 1.8.0 + typescript: 5.3.3 + workerpool: 6.5.1 + yargs: 17.7.2 + transitivePeerDependencies: + - supports-color + dev: false + + /jsii-srcmak@0.1.1000: + resolution: {integrity: sha512-b2C09k+CkZo5jE8cMMnv4Jt/VZ3iPA+5ks67m2ftB8fCcsG9iqWNTbcPLhzpPz0dvckOaR6bMwUSUODaBKXjzQ==} + hasBin: true + dependencies: + fs-extra: 9.1.0 + jsii: 5.3.3 + jsii-pacmak: 1.94.0 + ncp: 2.0.0 + yargs: 15.4.1 + transitivePeerDependencies: + - supports-color + dev: false + + /jsii@1.94.0: + resolution: {integrity: sha512-20KlKsBZlo7Ti6vfqTpKfZXnT2MKRGfh5bIPrwDODoCQmHNATfPFt1fs5+Wqd7xdrEj+A+sLAtjfHTw6i+sxCw==} + engines: {node: '>= 14.17.0'} + hasBin: true + dependencies: + '@jsii/check-node': 1.94.0 + '@jsii/spec': 1.94.0 + case: 1.6.3 + chalk: 4.1.2 + fast-deep-equal: 3.1.3 + fs-extra: 10.1.0 + log4js: 6.9.1 + semver: 7.5.4 + semver-intersect: 1.5.0 + sort-json: 2.0.1 + spdx-license-list: 6.8.0 + typescript: 3.9.10 + yargs: 16.2.0 + transitivePeerDependencies: + - supports-color + dev: false + + /jsii@5.3.3: + resolution: {integrity: sha512-M+kAUKJiLXXJXKYmBB0Q2n1aGoeNHyzMCLAx7402JqXSLxH4JGh6kOf4EH3U3LmQKzv2kxOHMRCg3Ssh82KtrQ==} + engines: {node: '>= 18.12.0'} + hasBin: true + dependencies: + '@jsii/check-node': 1.93.0 + '@jsii/spec': 1.94.0 + case: 1.6.3 + chalk: 4.1.2 + downlevel-dts: 0.11.0 + fast-deep-equal: 3.1.3 + log4js: 6.9.1 + semver: 7.5.4 + semver-intersect: 1.5.0 + sort-json: 2.0.1 + spdx-license-list: 6.8.0 + typescript: 5.3.3 + yargs: 17.7.2 + transitivePeerDependencies: + - supports-color + dev: false /json-buffer@3.0.1: resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} - dev: true + + /json-parse-better-errors@1.0.2: + resolution: {integrity: sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==} + dev: false /json-parse-even-better-errors@2.3.1: resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} - dev: true + + /json-parse-even-better-errors@3.0.1: + resolution: {integrity: sha512-aatBvbL26wVUCLmbWdCpeu9iF5wOyWpagiKkInA+kfws3sWdBrTnsvN2CKcyCYyUrc7rebNBlK6+kteg7ksecg==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + dev: false /json-schema-traverse@0.4.1: resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} - dev: true + + /json-schema-traverse@1.0.0: + resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + dev: false /json-stable-stringify-without-jsonify@1.0.1: resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} - dev: true + + /json-stable-stringify@1.1.0: + resolution: {integrity: sha512-zfA+5SuwYN2VWqN1/5HZaDzQKLJHaBVMZIIM+wuYjdptkaQsqzDdqjqf+lZZJUuJq1aanHiY8LhH8LmH+qBYJA==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.5 + isarray: 2.0.5 + jsonify: 0.0.1 + object-keys: 1.1.1 + dev: false + + /json-stringify-safe@5.0.1: + resolution: {integrity: sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==} + dev: false /json5@1.0.2: resolution: {integrity: sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==} @@ -2243,7 +10136,29 @@ packages: resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} engines: {node: '>=6'} hasBin: true - dev: true + + /jsonfile@4.0.0: + resolution: {integrity: sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==} + optionalDependencies: + graceful-fs: 4.2.11 + dev: false + + /jsonfile@6.1.0: + resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==} + dependencies: + universalify: 2.0.1 + optionalDependencies: + graceful-fs: 4.2.11 + dev: false + + /jsonify@0.0.1: + resolution: {integrity: sha512-2/Ki0GcmuqSrgFyelQq9M05y7PS0mEwuIzrf3f1fPqkVDVRvZrPZtVSMHxdgo8Aq0sxAOb/cr2aqqA3LeWHVPg==} + dev: false + + /jsonparse@1.3.1: + resolution: {integrity: sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==} + engines: {'0': node >= 0.2.0} + dev: false /jsx-ast-utils@3.3.5: resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==} @@ -2255,11 +10170,27 @@ packages: object.values: 1.1.7 dev: true + /just-extend@4.2.1: + resolution: {integrity: sha512-g3UB796vUFIY90VIv/WX3L2c8CS2MdWUww3CNrYmqza1Fg0DURc2K/O4YrnklBdQarSJ/y8JnJYDGc+1iumQjg==} + dev: true + /keyv@4.5.4: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} dependencies: json-buffer: 3.0.1 - dev: true + + /kind-of@6.0.3: + resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==} + engines: {node: '>=0.10.0'} + dev: false + + /kleur@3.0.3: + resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==} + engines: {node: '>=6'} + + /kuler@2.0.0: + resolution: {integrity: sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==} + dev: false /language-subtag-registry@0.3.22: resolution: {integrity: sha512-tN0MCzyWnoz/4nHS6uxdlFWoUZT7ABptwKPQ52Ea7URk6vll88bWBVhodtnlfEuCcKWNGoc+uGbw1cwa9IKh/w==} @@ -2272,64 +10203,335 @@ packages: language-subtag-registry: 0.3.22 dev: true + /lazystream@1.0.1: + resolution: {integrity: sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==} + engines: {node: '>= 0.6.3'} + dependencies: + readable-stream: 2.3.8 + dev: false + + /leven@3.1.0: + resolution: {integrity: sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==} + engines: {node: '>=6'} + /levn@0.4.1: resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} engines: {node: '>= 0.8.0'} dependencies: prelude-ls: 1.2.1 type-check: 0.4.0 - dev: true + + /lilconfig@3.0.0: + resolution: {integrity: sha512-K2U4W2Ff5ibV7j7ydLr+zLAkIg5JJ4lPn1Ltsdt+Tz/IjQ8buJ55pZAxoP34lqIiwtF9iAvtLv3JGv7CAyAg+g==} + engines: {node: '>=14'} + dev: false /lines-and-columns@1.2.4: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} - dev: true + + /lines-and-columns@2.0.4: + resolution: {integrity: sha512-wM1+Z03eypVAVUCE7QdSqpVIvelbOakn1M0bPDoA4SGWPx3sNDVUiMo3L6To6WWGClB7VyXnhQ4Sn7gxiJbE6A==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dev: false + + /load-json-file@4.0.0: + resolution: {integrity: sha512-Kx8hMakjX03tiGTLAIdJ+lL0htKnXjEZN6hk/tozf/WOuYGdZBJrZ+rCJRbVCugsjB3jMLn9746NsQIf5VjBMw==} + engines: {node: '>=4'} + dependencies: + graceful-fs: 4.2.11 + parse-json: 4.0.0 + pify: 3.0.0 + strip-bom: 3.0.0 + dev: false + + /load-tsconfig@0.2.5: + resolution: {integrity: sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dev: false + + /locate-path@2.0.0: + resolution: {integrity: sha512-NCI2kiDkyR7VeEKm27Kda/iQHyKJe1Bu0FlTbYp3CqJu+9IFe9bLyAjMxf5ZDDbEg+iMPzB5zYyUTSm8wVTKmA==} + engines: {node: '>=4'} + dependencies: + p-locate: 2.0.0 + path-exists: 3.0.0 + dev: false + + /locate-path@3.0.0: + resolution: {integrity: sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==} + engines: {node: '>=6'} + dependencies: + p-locate: 3.0.0 + path-exists: 3.0.0 + dev: false /locate-path@5.0.0: resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} engines: {node: '>=8'} dependencies: p-locate: 4.1.0 - dev: true /locate-path@6.0.0: resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} engines: {node: '>=10'} dependencies: p-locate: 5.0.0 + + /locate-path@7.2.0: + resolution: {integrity: sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dependencies: + p-locate: 6.0.0 + dev: false + + /lodash-es@4.17.21: + resolution: {integrity: sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==} + dev: false + + /lodash.capitalize@4.2.1: + resolution: {integrity: sha512-kZzYOKspf8XVX5AvmQF94gQW0lejFVgb80G85bU4ZWzoJ6C03PQg3coYAUpSTpQWelrZELd3XWgHzw4Ck5kaIw==} + dev: false + + /lodash.defaults@4.2.0: + resolution: {integrity: sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==} + dev: false + + /lodash.difference@4.5.0: + resolution: {integrity: sha512-dS2j+W26TQ7taQBGN8Lbbq04ssV3emRw4NY58WErlTO29pIqS0HmoT5aJ9+TUQ1N3G+JOZSji4eugsWwGp9yPA==} + dev: false + + /lodash.escaperegexp@4.1.2: + resolution: {integrity: sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw==} + dev: false + + /lodash.flatten@4.4.0: + resolution: {integrity: sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g==} + dev: false + + /lodash.get@4.4.2: + resolution: {integrity: sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==} dev: true + /lodash.isarguments@3.1.0: + resolution: {integrity: sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==} + dev: false + + /lodash.isequal@4.5.0: + resolution: {integrity: sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==} + dev: false + + /lodash.ismatch@4.4.0: + resolution: {integrity: sha512-fPMfXjGQEV9Xsq/8MTSgUf255gawYRbjwMyDbcvDhXgV7enSZA0hynz6vMPnpAb5iONEzBHBPsT+0zes5Z301g==} + dev: false + + /lodash.isplainobject@4.0.6: + resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==} + dev: false + + /lodash.isstring@4.0.1: + resolution: {integrity: sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==} + dev: false + + /lodash.mapvalues@4.6.0: + resolution: {integrity: sha512-JPFqXFeZQ7BfS00H58kClY7SPVeHertPE0lNuCyZ26/XlN8TvakYD7b9bGyNmXbT/D3BbtPAAmq90gPWqLkxlQ==} + dev: false + + /lodash.memoize@4.1.2: + resolution: {integrity: sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==} + /lodash.merge@4.6.2: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} - dev: true + + /lodash.sortby@4.7.0: + resolution: {integrity: sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==} + dev: false + + /lodash.union@4.6.0: + resolution: {integrity: sha512-c4pB2CdGrGdjMKYLA+XiRDO7Y0PRQbm/Gzg8qMj+QH+pFVAoTp5sBpO0odL3FjoPCGjK96p6qsP+yQoiLoOBcw==} + dev: false + + /lodash.uniqby@4.7.0: + resolution: {integrity: sha512-e/zcLx6CSbmaEgFHCA7BnoQKyCtKMxnuWrJygbwPs/AIn+IMKl66L8/s+wBUn5LRw2pZx3bUHibiV1b6aTWIww==} + dev: false /lodash@4.17.21: resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} - dev: true + + /log4js@6.9.1: + resolution: {integrity: sha512-1somDdy9sChrr9/f4UlzhdaGfDR2c/SaD2a4T7qEkG4jTS57/B3qmnjLYePwQ8cqWnUHZI0iAKxMBpCZICiZ2g==} + engines: {node: '>=8.0'} + dependencies: + date-format: 4.0.14 + debug: 4.3.4 + flatted: 3.2.9 + rfdc: 1.3.0 + streamroller: 3.1.5 + transitivePeerDependencies: + - supports-color + dev: false + + /logform@2.6.0: + resolution: {integrity: sha512-1ulHeNPp6k/LD8H91o7VYFBng5i1BDE7HoKxVbZiGFidS1Rj65qcywLxX+pVfAPoQJEjRdvKcusKwOupHCVOVQ==} + engines: {node: '>= 12.0.0'} + dependencies: + '@colors/colors': 1.6.0 + '@types/triple-beam': 1.3.5 + fecha: 4.2.3 + ms: 2.1.3 + safe-stable-stringify: 2.4.3 + triple-beam: 1.4.1 + dev: false + + /loglevel@1.8.1: + resolution: {integrity: sha512-tCRIJM51SHjAayKwC+QAg8hT8vg6z7GSgLJKGvzuPb1Wc+hLzqtuVLxp6/HzSPOozuK+8ErAhy7U/sVzw8Dgfg==} + engines: {node: '>= 0.6.0'} + dev: false + + /long@4.0.0: + resolution: {integrity: sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==} + dev: false /loose-envify@1.4.0: resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} hasBin: true dependencies: js-tokens: 4.0.0 + + /loupe@2.3.7: + resolution: {integrity: sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==} + dependencies: + get-func-name: 2.0.2 dev: true + /lowercase-keys@2.0.0: + resolution: {integrity: sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==} + engines: {node: '>=8'} + dev: false + + /lru-cache@10.1.0: + resolution: {integrity: sha512-/1clY/ui8CzjKFyjdvwPWJUYKiFVXG2I2cY0ssG7h4+hwk+XOIX7ZSG9Q7TW8TW3Kp3BUSqgFWBLgL4PJ+Blag==} + engines: {node: 14 || >=16.14} + dev: false + /lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} dependencies: yallist: 3.1.1 - dev: true /lru-cache@6.0.0: resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} engines: {node: '>=10'} dependencies: yallist: 4.0.0 - dev: true + + /lru-cache@7.18.3: + resolution: {integrity: sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==} + engines: {node: '>=12'} + dev: false + + /lru_map@0.3.3: + resolution: {integrity: sha512-Pn9cox5CsMYngeDbmChANltQl+5pi6XmTrraMSzhPmMBbmgcxmqWry0U3PGapCU1yB4/LqCcom7qhHZiF/jGfQ==} + dev: false + + /make-dir@4.0.0: + resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} + engines: {node: '>=10'} + dependencies: + semver: 7.5.4 + + /make-error@1.3.6: + resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==} + + /makeerror@1.0.12: + resolution: {integrity: sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==} + dependencies: + tmpl: 1.0.5 + + /map-obj@1.0.1: + resolution: {integrity: sha512-7N/q3lyZ+LVCp7PzuxrJr4KMbBE2hW7BT7YNia330OFxIf4d3r5zVpicP2650l7CPN6RM9zOJRl3NGpqSiw3Eg==} + engines: {node: '>=0.10.0'} + dev: false + + /map-obj@4.3.0: + resolution: {integrity: sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ==} + engines: {node: '>=8'} + dev: false + + /marked-terminal@5.2.0(marked@5.1.2): + resolution: {integrity: sha512-Piv6yNwAQXGFjZSaiNljyNFw7jKDdGrw70FSbtxEyldLsyeuV5ZHm/1wW++kWbrOF1VPnUgYOhB2oLL0ZpnekA==} + engines: {node: '>=14.13.1 || >=16.0.0'} + peerDependencies: + marked: ^1.0.0 || ^2.0.0 || ^3.0.0 || ^4.0.0 || ^5.0.0 + dependencies: + ansi-escapes: 6.2.0 + cardinal: 2.1.1 + chalk: 5.3.0 + cli-table3: 0.6.3 + marked: 5.1.2 + node-emoji: 1.11.0 + supports-hyperlinks: 2.3.0 + dev: false + + /marked@5.1.2: + resolution: {integrity: sha512-ahRPGXJpjMjwSOlBoTMZAK7ATXkli5qCPxZ21TG44rx1KEo44bii4ekgTDQPNRQ4Kh7JMb9Ub1PVk1NxRSsorg==} + engines: {node: '>= 16'} + hasBin: true + dev: false + + /md5@2.3.0: + resolution: {integrity: sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==} + dependencies: + charenc: 0.0.2 + crypt: 0.0.2 + is-buffer: 1.1.6 + dev: false + + /mdurl@1.0.1: + resolution: {integrity: sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g==} + dev: false + + /media-typer@0.3.0: + resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} + engines: {node: '>= 0.6'} + dev: false + + /meow@12.1.1: + resolution: {integrity: sha512-BhXM0Au22RwUneMPwSCnyhTOizdWoIEPU9sp0Aqa1PnDMR5Wv2FGXYDjuzJEIX+Eo2Rb8xuYe5jrnm5QowQFkw==} + engines: {node: '>=16.10'} + dev: false + + /meow@8.1.2: + resolution: {integrity: sha512-r85E3NdZ+mpYk1C6RjPFEMSE+s1iZMuHtsHAqY0DT3jZczl0diWUZ8g6oU7h0M9cD2EL+PzaYghhCLzR0ZNn5Q==} + engines: {node: '>=10'} + dependencies: + '@types/minimist': 1.2.5 + camelcase-keys: 6.2.2 + decamelize-keys: 1.1.1 + hard-rejection: 2.1.0 + minimist-options: 4.1.0 + normalize-package-data: 3.0.3 + read-pkg-up: 7.0.1 + redent: 3.0.0 + trim-newlines: 3.0.1 + type-fest: 0.18.1 + yargs-parser: 20.2.9 + dev: false + + /merge-descriptors@1.0.1: + resolution: {integrity: sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==} + dev: false + + /merge-stream@2.0.0: + resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} /merge2@1.4.1: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} engines: {node: '>= 8'} - dev: true + + /methods@1.1.2: + resolution: {integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==} + engines: {node: '>= 0.6'} + dev: false /micromatch@4.0.5: resolution: {integrity: sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==} @@ -2337,40 +10539,258 @@ packages: dependencies: braces: 3.0.2 picomatch: 2.3.1 - dev: true + + /mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + dev: false + + /mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + dependencies: + mime-db: 1.52.0 + dev: false + + /mime@1.6.0: + resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==} + engines: {node: '>=4'} + hasBin: true + dev: false + + /mime@2.6.0: + resolution: {integrity: sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==} + engines: {node: '>=4.0.0'} + hasBin: true + dev: false + + /mime@4.0.1: + resolution: {integrity: sha512-5lZ5tyrIfliMXzFtkYyekWbtRXObT9OWa8IwQ5uxTBDHucNNwniRqo0yInflj+iYi5CBa6qxadGzGarDfuEOxA==} + engines: {node: '>=16'} + hasBin: true + dev: false + + /mimic-fn@2.1.0: + resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} + engines: {node: '>=6'} + + /mimic-fn@4.0.0: + resolution: {integrity: sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==} + engines: {node: '>=12'} + dev: false + + /mimic-response@1.0.1: + resolution: {integrity: sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==} + engines: {node: '>=4'} + dev: false + + /mimic-response@3.1.0: + resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==} + engines: {node: '>=10'} + dev: false /min-indent@1.0.1: resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==} engines: {node: '>=4'} - dev: true /minimatch@3.1.2: resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} dependencies: brace-expansion: 1.1.11 - dev: true + + /minimatch@5.1.6: + resolution: {integrity: sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==} + engines: {node: '>=10'} + dependencies: + brace-expansion: 2.0.1 + dev: false + + /minimatch@9.0.3: + resolution: {integrity: sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==} + engines: {node: '>=16 || 14 >=14.17'} + dependencies: + brace-expansion: 2.0.1 + dev: false + + /minimist-options@4.1.0: + resolution: {integrity: sha512-Q4r8ghd80yhO/0j1O3B2BjweX3fiHg9cdOwjJd2J76Q135c+NDxGCqdYKQ1SKBuFfgWbAUzBfvYjPUEeNgqN1A==} + engines: {node: '>= 6'} + dependencies: + arrify: 1.0.1 + is-plain-obj: 1.1.0 + kind-of: 6.0.3 + dev: false /minimist@1.2.8: resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + /minipass@7.0.4: + resolution: {integrity: sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==} + engines: {node: '>=16 || 14 >=14.17'} + dev: false + + /mkdirp-classic@0.5.3: + resolution: {integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==} + dev: false + + /mnemonist@0.38.3: + resolution: {integrity: sha512-2K9QYubXx/NAjv4VLq1d1Ly8pWNC5L3BrixtdkyTegXWJIqY+zLNDhhX/A+ZwWt70tB1S8H4BE8FLYEFyNoOBw==} + dependencies: + obliterator: 1.6.1 + dev: false + + /modify-values@1.0.1: + resolution: {integrity: sha512-xV2bxeN6F7oYjZWTe/YPAy6MN2M+sL4u/Rlm2AHCIVGfo2p1yGmBHQ6vHehl4bRTZBdHu3TSkWdYgkwpYzAGSw==} + engines: {node: '>=0.10.0'} + dev: false + + /morgan@1.10.0: + resolution: {integrity: sha512-AbegBVI4sh6El+1gNwvD5YIck7nSA36weD7xvIxG4in80j/UoK8AEGaWnnz8v1GxonMCltmlNs5ZKbGvl9b1XQ==} + engines: {node: '>= 0.8.0'} + dependencies: + basic-auth: 2.0.1 + debug: 2.6.9 + depd: 2.0.0 + on-finished: 2.3.0 + on-headers: 1.0.2 + transitivePeerDependencies: + - supports-color + dev: false + + /ms@2.0.0: + resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==} + dev: false + /ms@2.1.2: resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} - dev: true /ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} - dev: true + + /mute-stream@1.0.0: + resolution: {integrity: sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + dev: false + + /mz@2.7.0: + resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} + dependencies: + any-promise: 1.3.0 + object-assign: 4.1.1 + thenify-all: 1.6.0 + dev: false + + /nan@2.18.0: + resolution: {integrity: sha512-W7tfG7vMOGtD30sHoZSSc/JVYiyDPEyQVso/Zz+/uQd0B0L46gtC+pHha5FFMRpil6fm/AoEcRWyOVi4+E/f8w==} + dev: false + + /napi-build-utils@1.0.2: + resolution: {integrity: sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==} + dev: false /natural-compare-lite@1.4.0: resolution: {integrity: sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==} - dev: true /natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + + /ncp@2.0.0: + resolution: {integrity: sha512-zIdGUrPRFTUELUvr3Gmc7KZ2Sw/h1PiVM0Af/oHB6zgnV1ikqSfRk+TOufi79aHYCW3NiOXmr1BP5nWbzojLaA==} + hasBin: true + dev: false + + /negotiator@0.6.3: + resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==} + engines: {node: '>= 0.6'} + dev: false + + /neo-async@2.6.2: + resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} + dev: false + + /nerf-dart@1.0.0: + resolution: {integrity: sha512-EZSPZB70jiVsivaBLYDCyntd5eH8NTSMOn3rB+HxwdmKThGELLdYv8qVIMWvZEFy9w8ZZpW9h9OB32l1rGtj7g==} + dev: false + + /nise@5.1.5: + resolution: {integrity: sha512-VJuPIfUFaXNRzETTQEEItTOP8Y171ijr+JLq42wHes3DiryR8vT+1TXQW/Rx8JNUhyYYWyIvjXTU6dOhJcs9Nw==} + dependencies: + '@sinonjs/commons': 2.0.0 + '@sinonjs/fake-timers': 10.3.0 + '@sinonjs/text-encoding': 0.7.2 + just-extend: 4.2.1 + path-to-regexp: 1.8.0 dev: true + /node-abi@3.54.0: + resolution: {integrity: sha512-p7eGEiQil0YUV3ItH4/tBb781L5impVmmx2E9FRKF7d18XXzp4PGT2tdYMFY6wQqgxD0IwNZOiSJ0/K0fSi/OA==} + engines: {node: '>=10'} + dependencies: + semver: 7.5.4 + dev: false + + /node-abort-controller@3.1.1: + resolution: {integrity: sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==} + dev: false + + /node-emoji@1.11.0: + resolution: {integrity: sha512-wo2DpQkQp7Sjm2A0cq+sN7EHKO6Sl0ctXeBdFZrL9T9+UywORbufTcTZxom8YqpLQt/FqNMUkOpkZrJVYSKD3A==} + dependencies: + lodash: 4.17.21 + dev: false + + /node-fetch@2.6.7: + resolution: {integrity: sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==} + engines: {node: 4.x || >=6.0.0} + peerDependencies: + encoding: ^0.1.0 + peerDependenciesMeta: + encoding: + optional: true + dependencies: + whatwg-url: 5.0.0 + dev: false + + /node-fetch@2.7.0: + resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} + engines: {node: 4.x || >=6.0.0} + peerDependencies: + encoding: ^0.1.0 + peerDependenciesMeta: + encoding: + optional: true + dependencies: + whatwg-url: 5.0.0 + dev: false + + /node-int64@0.4.0: + resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==} + /node-releases@2.0.14: resolution: {integrity: sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==} + + /nodemon@2.0.22: + resolution: {integrity: sha512-B8YqaKMmyuCO7BowF1Z1/mkPqLk6cs/l63Ojtd6otKjMx47Dq1utxfRxcavH1I7VSaL8n5BUaoutadnsX3AAVQ==} + engines: {node: '>=8.10.0'} + hasBin: true + dependencies: + chokidar: 3.5.3 + debug: 3.2.7(supports-color@5.5.0) + ignore-by-default: 1.0.1 + minimatch: 3.1.2 + pstree.remy: 1.1.8 + semver: 5.7.2 + simple-update-notifier: 1.1.0 + supports-color: 5.5.0 + touch: 3.1.0 + undefsafe: 2.0.5 + dev: true + + /nopt@1.0.10: + resolution: {integrity: sha512-NWmpvLSqUrgrAC9HCuxEvb+PSloHpqVu+FqcO4eeF2h5qYRhA7ev6KvelyQAKtegUbC6RypJnlEOhd8vloNKYg==} + hasBin: true + dependencies: + abbrev: 1.1.1 dev: true /normalize-package-data@2.5.0: @@ -2380,21 +10800,154 @@ packages: resolve: 1.22.8 semver: 5.7.2 validate-npm-package-license: 3.0.4 - dev: true + + /normalize-package-data@3.0.3: + resolution: {integrity: sha512-p2W1sgqij3zMMyRC067Dg16bfzVH+w7hyegmpIvZ4JNjqtGOVAIvLmjBx3yP7YTe9vKJgkoNOPjwQGogDoMXFA==} + engines: {node: '>=10'} + dependencies: + hosted-git-info: 4.1.0 + is-core-module: 2.13.1 + semver: 7.5.4 + validate-npm-package-license: 3.0.4 + dev: false + + /normalize-package-data@6.0.0: + resolution: {integrity: sha512-UL7ELRVxYBHBgYEtZCXjxuD5vPxnmvMGq0jp/dGPKKrN7tfsBh2IY7TlJ15WWwdjRWD3RJbnsygUurTK3xkPkg==} + engines: {node: ^16.14.0 || >=18.0.0} + dependencies: + hosted-git-info: 7.0.1 + is-core-module: 2.13.1 + semver: 7.5.4 + validate-npm-package-license: 3.0.4 + dev: false + + /normalize-path@3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} + + /normalize-url@6.1.0: + resolution: {integrity: sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==} + engines: {node: '>=10'} + dev: false + + /normalize-url@8.0.0: + resolution: {integrity: sha512-uVFpKhj5MheNBJRTiMZ9pE/7hD1QTeEvugSJW/OmLzAp78PB5O6adfMNTvmfKhXBkvCzC+rqifWcVYpGFwTjnw==} + engines: {node: '>=14.16'} + dev: false + + /npm-run-path@4.0.1: + resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==} + engines: {node: '>=8'} + dependencies: + path-key: 3.1.1 + + /npm-run-path@5.2.0: + resolution: {integrity: sha512-W4/tgAXFqFA0iL7fk0+uQ3g7wkL8xJmx3XdK0VGb4cHW//eZTtKGvFBBoRKVTpY7n6ze4NL9ly7rgXcHufqXKg==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dependencies: + path-key: 4.0.0 + dev: false + + /npm@9.9.2: + resolution: {integrity: sha512-D3tV+W0PzJOlwo8YmO6fNzaB1CrMVYd1V+2TURF6lbCbmZKqMsYgeQfPVvqiM3zbNSJPhFEnmlEXIogH2Vq7PQ==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + hasBin: true + dev: false + bundledDependencies: + - '@isaacs/string-locale-compare' + - '@npmcli/arborist' + - '@npmcli/config' + - '@npmcli/fs' + - '@npmcli/map-workspaces' + - '@npmcli/package-json' + - '@npmcli/promise-spawn' + - '@npmcli/run-script' + - abbrev + - archy + - cacache + - chalk + - ci-info + - cli-columns + - cli-table3 + - columnify + - fastest-levenshtein + - fs-minipass + - glob + - graceful-fs + - hosted-git-info + - ini + - init-package-json + - is-cidr + - json-parse-even-better-errors + - libnpmaccess + - libnpmdiff + - libnpmexec + - libnpmfund + - libnpmhook + - libnpmorg + - libnpmpack + - libnpmpublish + - libnpmsearch + - libnpmteam + - libnpmversion + - make-fetch-happen + - minimatch + - minipass + - minipass-pipeline + - ms + - node-gyp + - nopt + - normalize-package-data + - npm-audit-report + - npm-install-checks + - npm-package-arg + - npm-pick-manifest + - npm-profile + - npm-registry-fetch + - npm-user-validate + - npmlog + - p-map + - pacote + - parse-conflict-json + - proc-log + - qrcode-terminal + - read + - semver + - sigstore + - spdx-expression-parse + - ssri + - supports-color + - tar + - text-table + - tiny-relative-date + - treeverse + - validate-npm-package-name + - which + - write-file-atomic /object-assign@4.1.1: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} - dev: true + + /object-hash@2.2.0: + resolution: {integrity: sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw==} + engines: {node: '>= 6'} + dev: false /object-inspect@1.13.1: resolution: {integrity: sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==} - dev: true + + /object-is@1.1.5: + resolution: {integrity: sha512-3cyDsyHgtmi7I7DfSSI2LDp6SK2lwvtbg0p0R1e0RvTqF5ceGx+K2dfSjm1bKDMVCFEDAQvy+o8c6a7VujOddw==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.5 + define-properties: 1.2.1 + dev: false /object-keys@1.1.1: resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} engines: {node: '>= 0.4'} - dev: true /object.assign@4.1.5: resolution: {integrity: sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ==} @@ -2404,7 +10957,6 @@ packages: define-properties: 1.2.1 has-symbols: 1.0.3 object-keys: 1.1.1 - dev: true /object.entries@1.1.7: resolution: {integrity: sha512-jCBs/0plmPsOnrKAfFQXRG2NFjlhZgjjcBLSmTnEhU8U6vVTsVe8ANeQJCHTl3gSsI4J+0emOoCgoKlmQPMgmA==} @@ -2449,11 +11001,76 @@ packages: es-abstract: 1.22.3 dev: true + /obliterator@1.6.1: + resolution: {integrity: sha512-9WXswnqINnnhOG/5SLimUlzuU1hFJUc8zkwyD59Sd+dPOMf05PmnYG/d6Q7HZ+KmgkZJa1PxRso6QdM3sTNHig==} + dev: false + + /obliterator@2.0.4: + resolution: {integrity: sha512-lgHwxlxV1qIg1Eap7LgIeoBWIMFibOjbrYPIPJZcI1mmGAI2m3lNYpK12Y+GBdPQ0U1hRwSord7GIaawz962qQ==} + dev: false + + /on-finished@2.3.0: + resolution: {integrity: sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==} + engines: {node: '>= 0.8'} + dependencies: + ee-first: 1.1.1 + dev: false + + /on-finished@2.4.1: + resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} + engines: {node: '>= 0.8'} + dependencies: + ee-first: 1.1.1 + dev: false + + /on-headers@1.0.2: + resolution: {integrity: sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==} + engines: {node: '>= 0.8'} + dev: false + /once@1.4.0: resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} dependencies: wrappy: 1.0.2 - dev: true + + /one-time@1.0.0: + resolution: {integrity: sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==} + dependencies: + fn.name: 1.1.0 + dev: false + + /onetime@5.1.2: + resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} + engines: {node: '>=6'} + dependencies: + mimic-fn: 2.1.0 + + /onetime@6.0.0: + resolution: {integrity: sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==} + engines: {node: '>=12'} + dependencies: + mimic-fn: 4.0.0 + dev: false + + /oo-ascii-tree@1.94.0: + resolution: {integrity: sha512-i6UllReifEW2InBJHVFJNxrledRp3yr/yKVbpDmgWTguRe8/7BtBK3njzjvZNcPLEAtiWWxr0o9SpwYjapmTOw==} + engines: {node: '>= 14.17.0'} + dev: false + + /open@7.4.2: + resolution: {integrity: sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q==} + engines: {node: '>=8'} + dependencies: + is-docker: 2.2.1 + is-wsl: 2.2.0 + dev: false + + /optimism@0.16.2: + resolution: {integrity: sha512-zWNbgWj+3vLEjZNIh/okkY2EUfX+vB9TJopzIZwT1xxaMqC5hRLLraePod4c5n4He08xuXNH+zhKFFCu390wiQ==} + dependencies: + '@wry/context': 0.7.4 + '@wry/trie': 0.3.2 + dev: false /optionator@0.9.3: resolution: {integrity: sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==} @@ -2465,47 +11082,158 @@ packages: levn: 0.4.1 prelude-ls: 1.2.1 type-check: 0.4.0 - dev: true + + /os-tmpdir@1.0.2: + resolution: {integrity: sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==} + engines: {node: '>=0.10.0'} + dev: false + + /p-cancelable@2.1.1: + resolution: {integrity: sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==} + engines: {node: '>=8'} + dev: false + + /p-each-series@3.0.0: + resolution: {integrity: sha512-lastgtAdoH9YaLyDa5i5z64q+kzOcQHsQ5SsZJD3q0VEyI8mq872S3geuNbRUQLVAE9siMfgKrpj7MloKFHruw==} + engines: {node: '>=12'} + dev: false + + /p-filter@4.1.0: + resolution: {integrity: sha512-37/tPdZ3oJwHaS3gNJdenCDB3Tz26i9sjhnguBtvN0vYlRIiDNnvTWkuh+0hETV9rLPdJ3rlL3yVOYPIAnM8rw==} + engines: {node: '>=18'} + dependencies: + p-map: 7.0.1 + dev: false + + /p-is-promise@3.0.0: + resolution: {integrity: sha512-Wo8VsW4IRQSKVXsJCn7TomUaVtyfjVDn3nUP7kE967BQk0CwFpdbZs0X0uk5sW9mkBa9eNM7hCMaG93WUAwxYQ==} + engines: {node: '>=8'} + dev: false + + /p-limit@1.3.0: + resolution: {integrity: sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==} + engines: {node: '>=4'} + dependencies: + p-try: 1.0.0 + dev: false /p-limit@2.3.0: resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} engines: {node: '>=6'} dependencies: p-try: 2.2.0 - dev: true /p-limit@3.1.0: resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} engines: {node: '>=10'} dependencies: yocto-queue: 0.1.0 - dev: true + + /p-limit@4.0.0: + resolution: {integrity: sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dependencies: + yocto-queue: 1.0.0 + dev: false + + /p-locate@2.0.0: + resolution: {integrity: sha512-nQja7m7gSKuewoVRen45CtVfODR3crN3goVQ0DDZ9N3yHxgpkuBhZqsaiotSQRrADUrne346peY7kT3TSACykg==} + engines: {node: '>=4'} + dependencies: + p-limit: 1.3.0 + dev: false + + /p-locate@3.0.0: + resolution: {integrity: sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==} + engines: {node: '>=6'} + dependencies: + p-limit: 2.3.0 + dev: false /p-locate@4.1.0: resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} engines: {node: '>=8'} dependencies: p-limit: 2.3.0 - dev: true /p-locate@5.0.0: resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} engines: {node: '>=10'} dependencies: p-limit: 3.1.0 - dev: true + + /p-locate@6.0.0: + resolution: {integrity: sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dependencies: + p-limit: 4.0.0 + dev: false + + /p-map@2.1.0: + resolution: {integrity: sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==} + engines: {node: '>=6'} + dev: false + + /p-map@7.0.1: + resolution: {integrity: sha512-2wnaR0XL/FDOj+TgpDuRb2KTjLnu3Fma6b1ZUwGY7LcqenMcvP/YFpjpbPKY6WVGsbuJZRuoUz8iPrt8ORnAFw==} + engines: {node: '>=18'} + dev: false + + /p-reduce@2.1.0: + resolution: {integrity: sha512-2USApvnsutq8uoxZBGbbWM0JIYLiEMJ9RlaN7fAzVNb9OZN0SHjjTTfIcb667XynS5Y1VhwDJVDa72TnPzAYWw==} + engines: {node: '>=8'} + dev: false + + /p-reduce@3.0.0: + resolution: {integrity: sha512-xsrIUgI0Kn6iyDYm9StOpOeK29XM1aboGji26+QEortiFST1hGZaUQOLhtEbqHErPpGW/aSz6allwK2qcptp0Q==} + engines: {node: '>=12'} + dev: false + + /p-try@1.0.0: + resolution: {integrity: sha512-U1etNYuMJoIz3ZXSrrySFjsXQTWOx2/jdi86L+2pRvph/qMKL6sbcCYdH23fqsbm8TH2Gn0OybpT4eSFlCVHww==} + engines: {node: '>=4'} + dev: false /p-try@2.2.0: resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} engines: {node: '>=6'} - dev: true + + /package-up@5.0.0: + resolution: {integrity: sha512-MQEgDUvXCa3sGvqHg3pzHO8e9gqTCMPVrWUko3vPQGntwegmFo52mZb2abIVTjFnUcW0BcPz0D93jV5Cas1DWA==} + engines: {node: '>=18'} + dependencies: + find-up-simple: 1.0.0 + dev: false /parent-module@1.0.1: resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} engines: {node: '>=6'} dependencies: callsites: 3.1.0 - dev: true + + /parse-domain@5.0.0: + resolution: {integrity: sha512-sjvhVD0seIF3IquDLsbOE+6nekYyPzj4mGOMv4r9HIXalOjtTXb1uTZwtpQyp0myIoi9++w2eDqYOlCT5ic3+Q==} + hasBin: true + dependencies: + is-ip: 3.1.0 + node-fetch: 2.7.0 + punycode: 2.3.1 + transitivePeerDependencies: + - encoding + dev: false + + /parse-gitignore@1.0.1: + resolution: {integrity: sha512-UGyowyjtx26n65kdAMWhm6/3uy5uSrpcuH7tt+QEVudiBoVS+eqHxD5kbi9oWVRwj7sCzXqwuM+rUGw7earl6A==} + engines: {node: '>=6'} + dev: false + + /parse-json@4.0.0: + resolution: {integrity: sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw==} + engines: {node: '>=4'} + dependencies: + error-ex: 1.3.2 + json-parse-better-errors: 1.0.2 + dev: false /parse-json@5.2.0: resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} @@ -2515,56 +11243,193 @@ packages: error-ex: 1.3.2 json-parse-even-better-errors: 2.3.1 lines-and-columns: 1.2.4 - dev: true + + /parse-json@7.1.1: + resolution: {integrity: sha512-SgOTCX/EZXtZxBE5eJ97P4yGM5n37BwRU+YMsH4vNzFqJV/oWFXXCmwFlgWUM4PrakybVOueJJ6pwHqSVhTFDw==} + engines: {node: '>=16'} + dependencies: + '@babel/code-frame': 7.23.5 + error-ex: 1.3.2 + json-parse-even-better-errors: 3.0.1 + lines-and-columns: 2.0.4 + type-fest: 3.13.1 + dev: false + + /parseurl@1.3.3: + resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} + engines: {node: '>= 0.8'} + dev: false + + /patch-console@1.0.0: + resolution: {integrity: sha512-nxl9nrnLQmh64iTzMfyylSlRozL7kAXIaxw1fVcLYdyhNkJCRUzirRZTikXGJsg+hc4fqpneTK6iU2H1Q8THSA==} + engines: {node: '>=10'} + dev: false + + /path-exists@3.0.0: + resolution: {integrity: sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==} + engines: {node: '>=4'} + dev: false /path-exists@4.0.0: resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} engines: {node: '>=8'} - dev: true + + /path-exists@5.0.0: + resolution: {integrity: sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dev: false /path-is-absolute@1.0.1: resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} engines: {node: '>=0.10.0'} - dev: true /path-key@3.1.1: resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} engines: {node: '>=8'} + /path-key@4.0.0: + resolution: {integrity: sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==} + engines: {node: '>=12'} + dev: false + /path-parse@1.0.7: resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + + /path-scurry@1.10.1: + resolution: {integrity: sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ==} + engines: {node: '>=16 || 14 >=14.17'} + dependencies: + lru-cache: 10.1.0 + minipass: 7.0.4 + dev: false + + /path-to-regexp@0.1.7: + resolution: {integrity: sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==} + dev: false + + /path-to-regexp@1.8.0: + resolution: {integrity: sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==} + dependencies: + isarray: 0.0.1 dev: true /path-type@4.0.0: resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} engines: {node: '>=8'} + + /path-type@5.0.0: + resolution: {integrity: sha512-5HviZNaZcfqP95rwpv+1HDgUamezbqdSYTyzjTvwtJSnIH+3vnbmWsItli8OFEndS984VT55M3jduxZbX351gg==} + engines: {node: '>=12'} + dev: false + + /pathval@1.1.1: + resolution: {integrity: sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==} dev: true + /pend@1.2.0: + resolution: {integrity: sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==} + dev: false + /picocolors@1.0.0: resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==} - dev: true /picomatch@2.3.1: resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} engines: {node: '>=8.6'} - dev: true + + /pidtree@0.6.0: + resolution: {integrity: sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==} + engines: {node: '>=0.10'} + hasBin: true + dev: false + + /pidusage@3.0.2: + resolution: {integrity: sha512-g0VU+y08pKw5M8EZ2rIGiEBaB8wrQMjYGFfW2QVIfyT8V+fq8YFLkvlz4bz5ljvFDJYNFCWT3PWqcRr2FKO81w==} + engines: {node: '>=10'} + dependencies: + safe-buffer: 5.2.1 + dev: false + + /pify@3.0.0: + resolution: {integrity: sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==} + engines: {node: '>=4'} + dev: false + + /pirates@4.0.6: + resolution: {integrity: sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==} + engines: {node: '>= 6'} + + /pkg-conf@2.1.0: + resolution: {integrity: sha512-C+VUP+8jis7EsQZIhDYmS5qlNtjv2yP4SNtjXK9AP1ZcTRlnSfuumaTnRfYZnYgUUYVIKqL0fRvmUGDV2fmp6g==} + engines: {node: '>=4'} + dependencies: + find-up: 2.1.0 + load-json-file: 4.0.0 + dev: false + + /pkg-dir@4.2.0: + resolution: {integrity: sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==} + engines: {node: '>=8'} + dependencies: + find-up: 4.1.0 + + /pkg-up@3.1.0: + resolution: {integrity: sha512-nDywThFk1i4BQK4twPQ6TA4RT8bDY96yeuCVBWL3ePARCiEKDRSrNGbFIgUJpLp+XeIR65v8ra7WuJOFUBtkMA==} + engines: {node: '>=8'} + dependencies: + find-up: 3.0.0 + dev: false /pluralize@8.0.0: resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==} engines: {node: '>=4'} dev: true + /postcss-load-config@4.0.2(ts-node@10.9.2): + resolution: {integrity: sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==} + engines: {node: '>= 14'} + peerDependencies: + postcss: '>=8.0.9' + ts-node: '>=9.0.0' + peerDependenciesMeta: + postcss: + optional: true + ts-node: + optional: true + dependencies: + lilconfig: 3.0.0 + ts-node: 10.9.2(@types/node@20.10.8)(typescript@5.3.3) + yaml: 2.3.4 + dev: false + + /prebuild-install@7.1.1: + resolution: {integrity: sha512-jAXscXWMcCK8GgCoHOfIr0ODh5ai8mj63L2nWrjuAgXE6tDyYGnx4/8o/rCgU+B4JSyZBKbeZqzhtwtC3ovxjw==} + engines: {node: '>=10'} + hasBin: true + dependencies: + detect-libc: 2.0.2 + expand-template: 2.0.3 + github-from-package: 0.0.0 + minimist: 1.2.8 + mkdirp-classic: 0.5.3 + napi-build-utils: 1.0.2 + node-abi: 3.54.0 + pump: 3.0.0 + rc: 1.2.8 + simple-get: 4.0.1 + tar-fs: 2.1.1 + tunnel-agent: 0.6.0 + dev: false + /prelude-ls@1.2.1: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} - dev: true /prettier-linter-helpers@1.0.0: resolution: {integrity: sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==} engines: {node: '>=6.0.0'} dependencies: fast-diff: 1.3.0 - dev: true /prettier-plugin-packagejson@2.4.8(prettier@3.0.0): resolution: {integrity: sha512-ZK37c6pRUKeUIpQWNEdMgNUiGSG5BTfeeAIA01mRjVGTfWxxVzM55Cs+LaHyweFJbEgkgCNsqMA3LGEAjfOPtA==} @@ -2579,32 +11444,189 @@ packages: synckit: 0.8.8 dev: true + /prettier@2.8.8: + resolution: {integrity: sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==} + engines: {node: '>=10.13.0'} + hasBin: true + dev: false + /prettier@3.0.0: resolution: {integrity: sha512-zBf5eHpwHOGPC47h0zrPyNn+eAEIdEzfywMoYn2XPi0P44Zp0tSq64rq0xAREh4auw2cJZHo9QUob+NqCQky4g==} engines: {node: '>=14'} hasBin: true + + /pretty-format@28.1.3: + resolution: {integrity: sha512-8gFb/To0OmxHR9+ZTb14Df2vNxdGCX8g1xWGUTqUw5TiZvcQf5sHKObd5UcPyLLyowNwDAMTF3XWOG1B6mxl1Q==} + engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} + dependencies: + '@jest/schemas': 28.1.3 + ansi-regex: 5.0.1 + ansi-styles: 5.2.0 + react-is: 18.2.0 dev: true + /pretty-format@29.7.0: + resolution: {integrity: sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/schemas': 29.6.3 + ansi-styles: 5.2.0 + react-is: 18.2.0 + + /process-nextick-args@2.0.1: + resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} + dev: false + + /prompts@2.4.2: + resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==} + engines: {node: '>= 6'} + dependencies: + kleur: 3.0.3 + sisteransi: 1.0.5 + /prop-types@15.8.1: resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} dependencies: loose-envify: 1.4.0 object-assign: 4.1.1 react-is: 16.13.1 + + /proto-list@1.2.4: + resolution: {integrity: sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==} + dev: false + + /proxy-addr@2.0.7: + resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} + engines: {node: '>= 0.10'} + dependencies: + forwarded: 0.2.0 + ipaddr.js: 1.9.1 + dev: false + + /pstree.remy@1.1.8: + resolution: {integrity: sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==} dev: true + /pump@3.0.0: + resolution: {integrity: sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==} + dependencies: + end-of-stream: 1.4.4 + once: 1.4.0 + dev: false + /punycode@2.3.1: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} - dev: true + + /pure-rand@6.0.4: + resolution: {integrity: sha512-LA0Y9kxMYv47GIPJy6MI84fqTd2HmYZI83W/kM/SkKfDlajnZYfmXFTxkbY+xSBPkLJxltMa9hIkmdc29eguMA==} + + /qs@6.11.0: + resolution: {integrity: sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==} + engines: {node: '>=0.6'} + dependencies: + side-channel: 1.0.4 + dev: false + + /qs@6.11.2: + resolution: {integrity: sha512-tDNIz22aBzCDxLtVH++VnTfzxlfeK5CbqohpSqpJgj1Wg/cQbStNAz3NuqCs5vV+pjBsK4x4pN9HlVh7rcYRiA==} + engines: {node: '>=0.6'} + dependencies: + side-channel: 1.0.4 + dev: false /queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} - dev: true + + /quick-lru@4.0.1: + resolution: {integrity: sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g==} + engines: {node: '>=8'} + dev: false + + /quick-lru@5.1.1: + resolution: {integrity: sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==} + engines: {node: '>=10'} + dev: false + + /range-parser@1.2.1: + resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} + engines: {node: '>= 0.6'} + dev: false + + /raw-body@2.5.1: + resolution: {integrity: sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==} + engines: {node: '>= 0.8'} + dependencies: + bytes: 3.1.2 + http-errors: 2.0.0 + iconv-lite: 0.4.24 + unpipe: 1.0.0 + dev: false + + /raw-body@2.5.2: + resolution: {integrity: sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==} + engines: {node: '>= 0.8'} + dependencies: + bytes: 3.1.2 + http-errors: 2.0.0 + iconv-lite: 0.4.24 + unpipe: 1.0.0 + dev: false + + /rc@1.2.8: + resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==} + hasBin: true + dependencies: + deep-extend: 0.6.0 + ini: 1.3.8 + minimist: 1.2.8 + strip-json-comments: 2.0.1 + dev: false + + /react-devtools-core@4.28.5: + resolution: {integrity: sha512-cq/o30z9W2Wb4rzBefjv5fBalHU0rJGZCHAkf/RHSBWSSYwh8PlQTqqOJmgIIbBtpj27T6FIPXeomIjZtCNVqA==} + dependencies: + shell-quote: 1.8.1 + ws: 7.5.9 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + dev: false /react-is@16.13.1: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} - dev: true + + /react-is@18.2.0: + resolution: {integrity: sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==} + + /react-reconciler@0.26.2(react@17.0.2): + resolution: {integrity: sha512-nK6kgY28HwrMNwDnMui3dvm3rCFjZrcGiuwLc5COUipBK5hWHLOxMJhSnSomirqWwjPBJKV1QcbkI0VJr7Gl1Q==} + engines: {node: '>=0.10.0'} + peerDependencies: + react: ^17.0.2 + dependencies: + loose-envify: 1.4.0 + object-assign: 4.1.1 + react: 17.0.2 + scheduler: 0.20.2 + dev: false + + /react@17.0.2: + resolution: {integrity: sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA==} + engines: {node: '>=0.10.0'} + dependencies: + loose-envify: 1.4.0 + object-assign: 4.1.1 + dev: false + + /read-pkg-up@10.1.0: + resolution: {integrity: sha512-aNtBq4jR8NawpKJQldrQcSW9y/d+KWH4v24HWkHljOZ7H0av+YTGANBzRh9A5pw7v/bLVsLVPpOhJ7gHNVy8lA==} + engines: {node: '>=16'} + dependencies: + find-up: 6.3.0 + read-pkg: 8.1.0 + type-fest: 4.9.0 + dev: false /read-pkg-up@7.0.1: resolution: {integrity: sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==} @@ -2613,7 +11635,6 @@ packages: find-up: 4.1.0 read-pkg: 5.2.0 type-fest: 0.8.1 - dev: true /read-pkg@5.2.0: resolution: {integrity: sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==} @@ -2623,7 +11644,86 @@ packages: normalize-package-data: 2.5.0 parse-json: 5.2.0 type-fest: 0.6.0 - dev: true + + /read-pkg@8.1.0: + resolution: {integrity: sha512-PORM8AgzXeskHO/WEv312k9U03B8K9JSiWF/8N9sUuFjBa+9SF2u6K7VClzXwDXab51jCd8Nd36CNM+zR97ScQ==} + engines: {node: '>=16'} + dependencies: + '@types/normalize-package-data': 2.4.4 + normalize-package-data: 6.0.0 + parse-json: 7.1.1 + type-fest: 4.9.0 + dev: false + + /readable-stream@2.3.8: + resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==} + dependencies: + core-util-is: 1.0.3 + inherits: 2.0.4 + isarray: 1.0.0 + process-nextick-args: 2.0.1 + safe-buffer: 5.1.2 + string_decoder: 1.1.1 + util-deprecate: 1.0.2 + dev: false + + /readable-stream@3.6.2: + resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} + engines: {node: '>= 6'} + dependencies: + inherits: 2.0.4 + string_decoder: 1.3.0 + util-deprecate: 1.0.2 + dev: false + + /readdir-glob@1.1.3: + resolution: {integrity: sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==} + dependencies: + minimatch: 5.1.6 + dev: false + + /readdirp@3.6.0: + resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} + engines: {node: '>=8.10.0'} + dependencies: + picomatch: 2.3.1 + + /rechoir@0.6.2: + resolution: {integrity: sha512-HFM8rkZ+i3zrV+4LQjwQ0W+ez98pApMGM3HUrN04j3CqzPOzl9nmP15Y8YXNm8QHGv/eacOVEjqhmWpkRV0NAw==} + engines: {node: '>= 0.10'} + dependencies: + resolve: 1.22.8 + dev: false + + /redent@3.0.0: + resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==} + engines: {node: '>=8'} + dependencies: + indent-string: 4.0.0 + strip-indent: 3.0.0 + dev: false + + /redeyed@2.1.1: + resolution: {integrity: sha512-FNpGGo1DycYAdnrKFxCMmKYgo/mILAqtRYbkdQD8Ep/Hk2PQ5+aEAEx+IU713RTDmuBaH0c8P5ZozurNu5ObRQ==} + dependencies: + esprima: 4.0.1 + dev: false + + /redis-commands@1.7.0: + resolution: {integrity: sha512-nJWqw3bTFy21hX/CPKHth6sfhZbdiHP6bTawSgQBlKOVRG7EZkfHbbHwQJnrE4vsQf0CMNE+3gJ4Fmm16vdVlQ==} + dev: false + + /redis-errors@1.2.0: + resolution: {integrity: sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==} + engines: {node: '>=4'} + dev: false + + /redis-parser@3.0.0: + resolution: {integrity: sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==} + engines: {node: '>=4'} + dependencies: + redis-errors: 1.2.0 + dev: false /reflect.getprototypeof@1.0.4: resolution: {integrity: sha512-ECkTw8TmJwW60lOTR+ZkODISW6RQ8+2CL3COqtiJKLd6MmB45hN51HprHFziKLGkAuTGQhBb91V8cy+KHlaCjw==} @@ -2653,17 +11753,62 @@ packages: call-bind: 1.0.5 define-properties: 1.2.1 set-function-name: 2.0.1 - dev: true + + /registry-auth-token@5.0.2: + resolution: {integrity: sha512-o/3ikDxtXaA59BmZuZrJZDJv8NMDGSj+6j6XaeBmHw8eY1i1qd9+6H+LjVvQXx3HN6aRCGa1cUdJ9RaJZUugnQ==} + engines: {node: '>=14'} + dependencies: + '@pnpm/npm-conf': 2.2.2 + dev: false + + /require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + + /require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + dev: false + + /require-main-filename@2.0.0: + resolution: {integrity: sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==} + dev: false + + /reserved-words@0.1.2: + resolution: {integrity: sha512-0S5SrIUJ9LfpbVl4Yzij6VipUdafHrOTzvmfazSw/jeZrZtQK303OPZW+obtkaw7jQlTQppy0UvZWm9872PbRw==} + dev: false + + /resolve-alpn@1.2.1: + resolution: {integrity: sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==} + dev: false + + /resolve-cwd@3.0.0: + resolution: {integrity: sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==} + engines: {node: '>=8'} + dependencies: + resolve-from: 5.0.0 /resolve-from@4.0.0: resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} engines: {node: '>=4'} - dev: true + + /resolve-from@5.0.0: + resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} + engines: {node: '>=8'} /resolve-pkg-maps@1.0.0: resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} dev: true + /resolve.exports@1.1.1: + resolution: {integrity: sha512-/NtpHNDN7jWhAaQ9BvBUYZ6YTXsRBgfqWFWP7BZBaoMJO/I3G5OFzvTuWNlZC3aPjins1F+TNrLKsGbH4rfsRQ==} + engines: {node: '>=10'} + dev: true + + /resolve.exports@2.0.2: + resolution: {integrity: sha512-X2UW6Nw3n/aMgDVy+0rSqgHlv39WZAlZrXCdnbyEiKm17DSqHX4MmQMaST3FbeWR5FTuRcUwYAziZajji0Y7mg==} + engines: {node: '>=10'} + /resolve@1.19.0: resolution: {integrity: sha512-rArEXAgsBG4UgRGcynxWIWKFvh/XZCcS8UJdHhwy91zwAvCZIbcs+vAbflgBnNjYMs/i/i+/Ux6IZhML1yPvxg==} dependencies: @@ -2678,7 +11823,6 @@ packages: is-core-module: 2.13.1 path-parse: 1.0.7 supports-preserve-symlinks-flag: 1.0.0 - dev: true /resolve@2.0.0-next.5: resolution: {integrity: sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==} @@ -2689,23 +11833,76 @@ packages: supports-preserve-symlinks-flag: 1.0.0 dev: true + /response-iterator@0.2.6: + resolution: {integrity: sha512-pVzEEzrsg23Sh053rmDUvLSkGXluZio0qu8VT6ukrYuvtjVfCbDZH9d6PGXb8HZfzdNZt8feXv/jvUzlhRgLnw==} + engines: {node: '>=0.8'} + dev: false + + /responselike@2.0.1: + resolution: {integrity: sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==} + dependencies: + lowercase-keys: 2.0.0 + dev: false + + /restore-cursor@3.1.0: + resolution: {integrity: sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==} + engines: {node: '>=8'} + dependencies: + onetime: 5.1.2 + signal-exit: 3.0.7 + dev: false + + /retry@0.13.1: + resolution: {integrity: sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==} + engines: {node: '>= 4'} + dev: false + /reusify@1.0.4: resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} - dev: true + + /rfdc@1.3.0: + resolution: {integrity: sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA==} + dev: false /rimraf@3.0.2: resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} hasBin: true dependencies: glob: 7.2.3 - dev: true + + /rollup@4.9.4: + resolution: {integrity: sha512-2ztU7pY/lrQyXSCnnoU4ICjT/tCG9cdH3/G25ERqE3Lst6vl2BCM5hL2Nw+sslAvAf+ccKsAq1SkKQALyqhR7g==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + dependencies: + '@types/estree': 1.0.5 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.9.4 + '@rollup/rollup-android-arm64': 4.9.4 + '@rollup/rollup-darwin-arm64': 4.9.4 + '@rollup/rollup-darwin-x64': 4.9.4 + '@rollup/rollup-linux-arm-gnueabihf': 4.9.4 + '@rollup/rollup-linux-arm64-gnu': 4.9.4 + '@rollup/rollup-linux-arm64-musl': 4.9.4 + '@rollup/rollup-linux-riscv64-gnu': 4.9.4 + '@rollup/rollup-linux-x64-gnu': 4.9.4 + '@rollup/rollup-linux-x64-musl': 4.9.4 + '@rollup/rollup-win32-arm64-msvc': 4.9.4 + '@rollup/rollup-win32-ia32-msvc': 4.9.4 + '@rollup/rollup-win32-x64-msvc': 4.9.4 + fsevents: 2.3.3 + dev: false + + /run-async@3.0.0: + resolution: {integrity: sha512-540WwVDOMxA6dN6We19EcT9sc3hkXPw5mzRNGM3FkdN/vtE9NFvj5lFAPNwUDmJjXidm3v7TC1cTE7t17Ulm1Q==} + engines: {node: '>=0.12.0'} + dev: false /run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} dependencies: queue-microtask: 1.2.3 - dev: true /safe-array-concat@1.0.1: resolution: {integrity: sha512-6XbUAseYE2KtOuGueyeobCySj9L4+66Tn6KQMOPQJrAJEowYKW/YR/MGJZl7FdydUdaFu4LYyDZjxf4/Nmo23Q==} @@ -2717,6 +11914,14 @@ packages: isarray: 2.0.5 dev: true + /safe-buffer@5.1.2: + resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} + dev: false + + /safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + dev: false + /safe-regex-test@1.0.0: resolution: {integrity: sha512-JBUUzyOgEwXQY1NuPtvcj/qcBDbDmEvWufhlnXZIm75DEHp+afM1r1ujJpJsV/gSM4t59tpDyPi1sd6ZaPFfsA==} dependencies: @@ -2731,14 +11936,93 @@ packages: regexp-tree: 0.1.27 dev: true + /safe-stable-stringify@2.4.3: + resolution: {integrity: sha512-e2bDA2WJT0wxseVd4lsDP4+3ONX6HpMXQa1ZhFQ7SU+GjvORCmShbCMltrtIDfkYhVHrOcPtj+KhmDBdPdZD1g==} + engines: {node: '>=10'} + dev: false + + /safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + dev: false + + /sax@1.3.0: + resolution: {integrity: sha512-0s+oAmw9zLl1V1cS9BtZN7JAd0cW5e0QH4W3LWEK6a4LaLEA2OTpGYWDY+6XasBLtz6wkm3u1xRw95mRuJ59WA==} + dev: false + + /scheduler@0.20.2: + resolution: {integrity: sha512-2eWfGgAqqWFGqtdMmcL5zCMK1U8KlXv8SQFGglL3CEtd0aDVDWgeF/YoCmvln55m5zSk3J/20hTaSBeSObsQDQ==} + dependencies: + loose-envify: 1.4.0 + object-assign: 4.1.1 + dev: false + + /semantic-release@21.1.2(typescript@5.3.3): + resolution: {integrity: sha512-kz76azHrT8+VEkQjoCBHE06JNQgTgsC4bT8XfCzb7DHcsk9vG3fqeMVik8h5rcWCYi2Fd+M3bwA7BG8Z8cRwtA==} + engines: {node: '>=18'} + hasBin: true + dependencies: + '@semantic-release/commit-analyzer': 10.0.4(semantic-release@21.1.2) + '@semantic-release/error': 4.0.0 + '@semantic-release/github': 9.2.6(semantic-release@21.1.2) + '@semantic-release/npm': 10.0.6(semantic-release@21.1.2) + '@semantic-release/release-notes-generator': 11.0.7(semantic-release@21.1.2) + aggregate-error: 5.0.0 + cosmiconfig: 8.3.6(typescript@5.3.3) + debug: 4.3.4 + env-ci: 9.1.1 + execa: 8.0.1 + figures: 5.0.0 + find-versions: 5.1.0 + get-stream: 6.0.1 + git-log-parser: 1.2.0 + hook-std: 3.0.0 + hosted-git-info: 7.0.1 + lodash-es: 4.17.21 + marked: 5.1.2 + marked-terminal: 5.2.0(marked@5.1.2) + micromatch: 4.0.5 + p-each-series: 3.0.0 + p-reduce: 3.0.0 + read-pkg-up: 10.1.0 + resolve-from: 5.0.0 + semver: 7.5.4 + semver-diff: 4.0.0 + signale: 1.4.0 + yargs: 17.7.2 + transitivePeerDependencies: + - supports-color + - typescript + dev: false + + /semver-diff@4.0.0: + resolution: {integrity: sha512-0Ju4+6A8iOnpL/Thra7dZsSlOHYAHIeMxfhWQRI1/VLcT3WDBZKKtQt/QkBOsiIN9ZpuvHE6cGZ0x4glCMmfiA==} + engines: {node: '>=12'} + dependencies: + semver: 7.5.4 + dev: false + + /semver-intersect@1.5.0: + resolution: {integrity: sha512-BDjWX7yCC0haX4W/zrnV2JaMpVirwaEkGOBmgRQtH++F1N3xl9v7k9H44xfTqwl+yLNNSbMKosoVSTIiJVQ2Pw==} + dependencies: + semver: 6.3.1 + dev: false + + /semver-regex@4.0.5: + resolution: {integrity: sha512-hunMQrEy1T6Jr2uEVjrAIqjwWcQTgOAcIM52C8MY1EZSD3DDNft04XzvYKPqjED65bNVVko0YI38nYeEHCX3yw==} + engines: {node: '>=12'} + dev: false + /semver@5.7.2: resolution: {integrity: sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==} hasBin: true - dev: true /semver@6.3.1: resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} hasBin: true + + /semver@7.0.0: + resolution: {integrity: sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A==} + hasBin: true dev: true /semver@7.5.4: @@ -2747,7 +12031,43 @@ packages: hasBin: true dependencies: lru-cache: 6.0.0 - dev: true + + /send@0.18.0: + resolution: {integrity: sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==} + engines: {node: '>= 0.8.0'} + dependencies: + debug: 2.6.9 + depd: 2.0.0 + destroy: 1.2.0 + encodeurl: 1.0.2 + escape-html: 1.0.3 + etag: 1.8.1 + fresh: 0.5.2 + http-errors: 2.0.0 + mime: 1.6.0 + ms: 2.1.3 + on-finished: 2.4.1 + range-parser: 1.2.1 + statuses: 2.0.1 + transitivePeerDependencies: + - supports-color + dev: false + + /serve-static@1.15.0: + resolution: {integrity: sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==} + engines: {node: '>= 0.8.0'} + dependencies: + encodeurl: 1.0.2 + escape-html: 1.0.3 + parseurl: 1.3.3 + send: 0.18.0 + transitivePeerDependencies: + - supports-color + dev: false + + /set-blocking@2.0.0: + resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==} + dev: false /set-function-length@1.1.1: resolution: {integrity: sha512-VoaqjbBJKiWtg4yRcKBQ7g7wnGnLV3M8oLvVWwOk2PdYY6PEFegR1vezXR0tw6fZGF9csVakIRjrJiy2veSBFQ==} @@ -2757,7 +12077,6 @@ packages: get-intrinsic: 1.2.2 gopd: 1.0.1 has-property-descriptors: 1.0.1 - dev: true /set-function-name@2.0.1: resolution: {integrity: sha512-tMNCiqYVkXIZgc2Hnoy2IvC/f8ezc5koaRFkCjrpWzGpCd3qbZXPzVy9MAZzK1ch/X0jvSkojys3oqJN0qCmdA==} @@ -2766,7 +12085,18 @@ packages: define-data-property: 1.1.1 functions-have-names: 1.2.3 has-property-descriptors: 1.0.1 - dev: true + + /setprototypeof@1.2.0: + resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} + dev: false + + /sha.js@2.4.11: + resolution: {integrity: sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==} + hasBin: true + dependencies: + inherits: 2.0.4 + safe-buffer: 5.2.1 + dev: false /shebang-command@2.0.0: resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} @@ -2778,24 +12108,121 @@ packages: resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} engines: {node: '>=8'} + /shell-quote@1.8.1: + resolution: {integrity: sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA==} + dev: false + + /shelljs@0.8.5: + resolution: {integrity: sha512-TiwcRcrkhHvbrZbnRcFYMLl30Dfov3HKqzp5tO5b4pt6G/SezKcYhmDg15zXVBswHmctSAQKznqNW2LO5tTDow==} + engines: {node: '>=4'} + hasBin: true + dependencies: + glob: 7.2.3 + interpret: 1.4.0 + rechoir: 0.6.2 + dev: false + /side-channel@1.0.4: resolution: {integrity: sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==} dependencies: call-bind: 1.0.5 get-intrinsic: 1.2.2 object-inspect: 1.13.1 + + /signal-exit@3.0.7: + resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} + + /signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + dev: false + + /signale@1.4.0: + resolution: {integrity: sha512-iuh+gPf28RkltuJC7W5MRi6XAjTDCAPC/prJUpQoG4vIP3MJZ+GTydVnodXA7pwvTKb2cA0m9OFZW/cdWy/I/w==} + engines: {node: '>=6'} + dependencies: + chalk: 2.4.2 + figures: 2.0.0 + pkg-conf: 2.1.0 + dev: false + + /simple-concat@1.0.1: + resolution: {integrity: sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==} + dev: false + + /simple-get@4.0.1: + resolution: {integrity: sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==} + dependencies: + decompress-response: 6.0.0 + once: 1.4.0 + simple-concat: 1.0.1 + dev: false + + /simple-swizzle@0.2.2: + resolution: {integrity: sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==} + dependencies: + is-arrayish: 0.3.2 + dev: false + + /simple-update-notifier@1.1.0: + resolution: {integrity: sha512-VpsrsJSUcJEseSbMHkrsrAVSdvVS5I96Qo1QAQ4FxQ9wXFcB+pjj7FB7/us9+GcgfW4ziHtYMc1J0PLczb55mg==} + engines: {node: '>=8.10.0'} + dependencies: + semver: 7.0.0 + dev: true + + /sinon@17.0.1: + resolution: {integrity: sha512-wmwE19Lie0MLT+ZYNpDymasPHUKTaZHUH/pKEubRXIzySv9Atnlw+BUMGCzWgV7b7wO+Hw6f1TEOr0IUnmU8/g==} + dependencies: + '@sinonjs/commons': 3.0.0 + '@sinonjs/fake-timers': 11.2.2 + '@sinonjs/samsam': 8.0.0 + diff: 5.1.0 + nise: 5.1.5 + supports-color: 7.2.0 dev: true + /sisteransi@1.0.5: + resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} + /slash@3.0.0: resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} engines: {node: '>=8'} - dev: true /slash@4.0.0: resolution: {integrity: sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==} engines: {node: '>=12'} dev: true + /slash@5.1.0: + resolution: {integrity: sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==} + engines: {node: '>=14.16'} + dev: false + + /slice-ansi@3.0.0: + resolution: {integrity: sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==} + engines: {node: '>=8'} + dependencies: + ansi-styles: 4.3.0 + astral-regex: 2.0.0 + is-fullwidth-code-point: 3.0.0 + dev: false + + /sort-any@2.0.0: + resolution: {integrity: sha512-T9JoiDewQEmWcnmPn/s9h/PH9t3d/LSWi0RgVmXSuDYeZXTZOZ1/wrK2PHaptuR1VXe3clLLt0pD6sgVOwjNEA==} + dependencies: + lodash: 4.17.21 + dev: false + + /sort-json@2.0.1: + resolution: {integrity: sha512-s8cs2bcsQCzo/P2T/uoU6Js4dS/jnX8+4xunziNoq9qmSpZNCrRIAIvp4avsz0ST18HycV4z/7myJ7jsHWB2XQ==} + hasBin: true + dependencies: + detect-indent: 5.0.0 + detect-newline: 2.1.0 + minimist: 1.2.8 + dev: false + /sort-object-keys@1.1.3: resolution: {integrity: sha512-855pvK+VkU7PaKYPc+Jjnmt4EzejQHyhhF33q31qG8x7maDzkeFhAAThdCYay11CISO+qAMwjOBP+fPZe0IPyg==} dev: true @@ -2813,27 +12240,157 @@ packages: sort-object-keys: 1.1.3 dev: true + /source-map-support@0.5.13: + resolution: {integrity: sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==} + dependencies: + buffer-from: 1.1.2 + source-map: 0.6.1 + + /source-map@0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + + /source-map@0.8.0-beta.0: + resolution: {integrity: sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==} + engines: {node: '>= 8'} + dependencies: + whatwg-url: 7.1.0 + dev: false + + /spawn-error-forwarder@1.0.0: + resolution: {integrity: sha512-gRjMgK5uFjbCvdibeGJuy3I5OYz6VLoVdsOJdA6wV0WlfQVLFueoqMxwwYD9RODdgb6oUIvlRlsyFSiQkMKu0g==} + dev: false + /spdx-correct@3.2.0: resolution: {integrity: sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==} dependencies: spdx-expression-parse: 3.0.1 spdx-license-ids: 3.0.16 - dev: true /spdx-exceptions@2.3.0: resolution: {integrity: sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==} - dev: true /spdx-expression-parse@3.0.1: resolution: {integrity: sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==} dependencies: spdx-exceptions: 2.3.0 spdx-license-ids: 3.0.16 - dev: true /spdx-license-ids@3.0.16: resolution: {integrity: sha512-eWN+LnM3GR6gPu35WxNgbGl8rmY1AEmoMDvL/QD6zYmPWgywxWqJWNdLGT+ke8dKNWrcYgYjPpG5gbTfghP8rw==} - dev: true + + /spdx-license-list@6.8.0: + resolution: {integrity: sha512-5UdM7r9yJ1EvsPQZWfa41AZjLQngl9iMMysm9XBW7Lqhq7aF8cllfqjS+rFCHB8FFMGSM0yFWue2LUV9mR0QzQ==} + engines: {node: '>=8'} + dev: false + + /split2@1.0.0: + resolution: {integrity: sha512-NKywug4u4pX/AZBB1FCPzZ6/7O+Xhz1qMVbzTvvKvikjO99oPN87SkK08mEY9P63/5lWjK+wgOOgApnTg5r6qg==} + dependencies: + through2: 2.0.5 + dev: false + + /split2@4.2.0: + resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} + engines: {node: '>= 10.x'} + dev: false + + /split@1.0.1: + resolution: {integrity: sha512-mTyOoPbrivtXnwnIxZRFYRrPNtEFKlpB2fvjSnCQUiAA6qAZzqwna5envK4uk6OIeP17CsdF3rSBGYVBsU0Tkg==} + dependencies: + through: 2.3.8 + dev: false + + /sprintf-js@1.0.3: + resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} + + /sscaff@1.2.274: + resolution: {integrity: sha512-sztRa50SL1LVxZnF1au6QT1SC2z0S1oEOyi2Kpnlg6urDns93aL32YxiJcNkLcY+VHFtVqm/SRv4cb+6LeoBQA==} + engines: {node: '>= 12.13.0'} + dev: false + + /stack-trace@0.0.10: + resolution: {integrity: sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==} + dev: false + + /stack-utils@2.0.6: + resolution: {integrity: sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==} + engines: {node: '>=10'} + dependencies: + escape-string-regexp: 2.0.0 + + /standard-as-callback@2.1.0: + resolution: {integrity: sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==} + dev: false + + /statuses@2.0.1: + resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} + engines: {node: '>= 0.8'} + dev: false + + /stop-iteration-iterator@1.0.0: + resolution: {integrity: sha512-iCGQj+0l0HOdZ2AEeBADlsRC+vsnDsZsbdSiH1yNSjcfKM7fdpCMfqAL/dwF5BLiw/XhRft/Wax6zQbhq2BcjQ==} + engines: {node: '>= 0.4'} + dependencies: + internal-slot: 1.0.6 + dev: false + + /stream-buffers@3.0.2: + resolution: {integrity: sha512-DQi1h8VEBA/lURbSwFtEHnSTb9s2/pwLEaFuNhXwy1Dx3Sa0lOuYT2yNUr4/j2fs8oCAMANtrZ5OrPZtyVs3MQ==} + engines: {node: '>= 0.10.0'} + dev: false + + /stream-chain@2.2.5: + resolution: {integrity: sha512-1TJmBx6aSWqZ4tx7aTpBDXK0/e2hhcNSTV8+CbFJtDjbb+I1mZ8lHit0Grw9GRT+6JbIrrDd8esncgBi8aBXGA==} + dev: false + + /stream-combiner2@1.1.1: + resolution: {integrity: sha512-3PnJbYgS56AeWgtKF5jtJRT6uFJe56Z0Hc5Ngg/6sI6rIt8iiMBTa9cvdyFfpMQjaVHr8dusbNeFGIIonxOvKw==} + dependencies: + duplexer2: 0.1.4 + readable-stream: 2.3.8 + dev: false + + /stream-json@1.8.0: + resolution: {integrity: sha512-HZfXngYHUAr1exT4fxlbc1IOce1RYxp2ldeaf97LYCOPSoOqY/1Psp7iGvpb+6JIOgkra9zDYnPX01hGAHzEPw==} + dependencies: + stream-chain: 2.2.5 + dev: false + + /streamroller@3.1.5: + resolution: {integrity: sha512-KFxaM7XT+irxvdqSP1LGLgNWbYN7ay5owZ3r/8t77p+EtSUAfUgtl7be3xtqtOmGUl9K9YPO2ca8133RlTjvKw==} + engines: {node: '>=8.0'} + dependencies: + date-format: 4.0.14 + debug: 4.3.4 + fs-extra: 8.1.0 + transitivePeerDependencies: + - supports-color + dev: false + + /string-length@4.0.2: + resolution: {integrity: sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==} + engines: {node: '>=10'} + dependencies: + char-regex: 1.0.2 + strip-ansi: 6.0.1 + + /string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + /string-width@5.1.2: + resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} + engines: {node: '>=12'} + dependencies: + eastasianwidth: 0.2.0 + emoji-regex: 9.2.2 + strip-ansi: 7.1.0 + dev: false /string.prototype.matchall@4.0.10: resolution: {integrity: sha512-rGXbGmOEosIQi6Qva94HUjgPs9vKW+dkG7Y8Q5O2OYkWL6wFaTRZO8zM4mhP94uX55wgyrXzfS2aGtGzUL7EJQ==} @@ -2849,6 +12406,10 @@ packages: side-channel: 1.0.4 dev: true + /string.prototype.repeat@0.2.0: + resolution: {integrity: sha512-1BH+X+1hSthZFW+X+JaUkjkkUPwIlLEMJBLANN3hOob3RhEk5snLWNECDnYbgn/m5c5JV7Ersu1Yubaf+05cIA==} + dev: false + /string.prototype.trim@1.2.8: resolution: {integrity: sha512-lfjY4HcixfQXOfaqCvcBuOIapyaroTXhbkfJN3gcB1OtyupngWK4sEET9Knd0cXd28kTUqu/kHoV4HKSJdnjiQ==} engines: {node: '>= 0.4'} @@ -2874,48 +12435,142 @@ packages: es-abstract: 1.22.3 dev: true + /string_decoder@1.1.1: + resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==} + dependencies: + safe-buffer: 5.1.2 + dev: false + + /string_decoder@1.3.0: + resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + dependencies: + safe-buffer: 5.2.1 + dev: false + /strip-ansi@6.0.1: resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} engines: {node: '>=8'} dependencies: ansi-regex: 5.0.1 - dev: true + + /strip-ansi@7.1.0: + resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==} + engines: {node: '>=12'} + dependencies: + ansi-regex: 6.0.1 + dev: false /strip-bom@3.0.0: resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} engines: {node: '>=4'} - dev: true + + /strip-bom@4.0.0: + resolution: {integrity: sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==} + engines: {node: '>=8'} + + /strip-final-newline@2.0.0: + resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==} + engines: {node: '>=6'} + + /strip-final-newline@3.0.0: + resolution: {integrity: sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==} + engines: {node: '>=12'} + dev: false /strip-indent@3.0.0: resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==} engines: {node: '>=8'} dependencies: min-indent: 1.0.1 - dev: true + + /strip-json-comments@2.0.1: + resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==} + engines: {node: '>=0.10.0'} + dev: false /strip-json-comments@3.1.1: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} engines: {node: '>=8'} - dev: true + + /strnum@1.0.5: + resolution: {integrity: sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA==} + dev: false + + /sucrase@3.35.0: + resolution: {integrity: sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==} + engines: {node: '>=16 || 14 >=14.17'} + hasBin: true + dependencies: + '@jridgewell/gen-mapping': 0.3.3 + commander: 4.1.1 + glob: 10.3.10 + lines-and-columns: 1.2.4 + mz: 2.7.0 + pirates: 4.0.6 + ts-interface-checker: 0.1.13 + dev: false + + /superagent@8.1.2: + resolution: {integrity: sha512-6WTxW1EB6yCxV5VFOIPQruWGHqc3yI7hEmZK6h+pyk69Lk/Ut7rLUY6W/ONF2MjBuGjvmMiIpsrVJ2vjrHlslA==} + engines: {node: '>=6.4.0 <13 || >=14'} + dependencies: + component-emitter: 1.3.1 + cookiejar: 2.1.4 + debug: 4.3.4 + fast-safe-stringify: 2.1.1 + form-data: 4.0.0 + formidable: 2.1.2 + methods: 1.1.2 + mime: 2.6.0 + qs: 6.11.2 + semver: 7.5.4 + transitivePeerDependencies: + - supports-color + dev: false + + /supertest@6.3.3: + resolution: {integrity: sha512-EMCG6G8gDu5qEqRQ3JjjPs6+FYT1a7Hv5ApHvtSghmOFJYtsU5S+pSb6Y2EUeCEY3CmEL3mmQ8YWlPOzQomabA==} + engines: {node: '>=6.4.0'} + dependencies: + methods: 1.1.2 + superagent: 8.1.2 + transitivePeerDependencies: + - supports-color + dev: false /supports-color@5.5.0: resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} engines: {node: '>=4'} dependencies: has-flag: 3.0.0 - dev: true /supports-color@7.2.0: resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} engines: {node: '>=8'} dependencies: has-flag: 4.0.0 - dev: true + + /supports-color@8.1.1: + resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==} + engines: {node: '>=10'} + dependencies: + has-flag: 4.0.0 + + /supports-hyperlinks@2.3.0: + resolution: {integrity: sha512-RpsAZlpWcDwOPQA22aCH4J0t7L8JmAvsCxfOSEwm7cQs3LshN36QaTkwd70DnBOXDWGssw2eUoc8CaRWT0XunA==} + engines: {node: '>=8'} + dependencies: + has-flag: 4.0.0 + supports-color: 7.2.0 /supports-preserve-symlinks-flag@1.0.0: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} - dev: true + + /symbol-observable@4.0.0: + resolution: {integrity: sha512-b19dMThMV4HVFynSAM1++gBHAbk2Tc/osgLIBZMKsyqh34jb2e8Os7T6ZW/Bt3pJFdBTd2JwAnAAEQV7rSNvcQ==} + engines: {node: '>=0.10'} + dev: false /synckit@0.8.8: resolution: {integrity: sha512-HwOKAP7Wc5aRGYdKH+dw0PRRpbO841v2DENBtjnR5HFWoiNByAl7vrx3p0G/rCyYXQsrxqtX48TImFtPcIHSpQ==} @@ -2923,37 +12578,287 @@ packages: dependencies: '@pkgr/core': 0.1.0 tslib: 2.6.2 - dev: true /tapable@2.2.1: resolution: {integrity: sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==} engines: {node: '>=6'} dev: true + /tar-fs@2.1.1: + resolution: {integrity: sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==} + dependencies: + chownr: 1.1.4 + mkdirp-classic: 0.5.3 + pump: 3.0.0 + tar-stream: 2.2.0 + dev: false + + /tar-stream@2.2.0: + resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==} + engines: {node: '>=6'} + dependencies: + bl: 4.1.0 + end-of-stream: 1.4.4 + fs-constants: 1.0.0 + inherits: 2.0.4 + readable-stream: 3.6.2 + dev: false + + /temp-dir@3.0.0: + resolution: {integrity: sha512-nHc6S/bwIilKHNRgK/3jlhDoIHcp45YgyiwcAk46Tr0LfEqGBVpmiAyuiuxeVE44m3mXnEeVhaipLOEWmH+Njw==} + engines: {node: '>=14.16'} + dev: false + + /tempy@3.1.0: + resolution: {integrity: sha512-7jDLIdD2Zp0bDe5r3D2qtkd1QOCacylBuL7oa4udvN6v2pqr4+LcCr67C8DR1zkpaZ8XosF5m1yQSabKAW6f2g==} + engines: {node: '>=14.16'} + dependencies: + is-stream: 3.0.0 + temp-dir: 3.0.0 + type-fest: 2.19.0 + unique-string: 3.0.0 + dev: false + + /terminal-link@2.1.1: + resolution: {integrity: sha512-un0FmiRUQNr5PJqy9kP7c40F5BOfpGlYTrxonDChEZB7pzZxRNp/bt+ymiy9/npwXya9KH99nJ/GXFIiUkYGFQ==} + engines: {node: '>=8'} + dependencies: + ansi-escapes: 4.3.2 + supports-hyperlinks: 2.3.0 + dev: true + + /test-exclude@6.0.0: + resolution: {integrity: sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==} + engines: {node: '>=8'} + dependencies: + '@istanbuljs/schema': 0.1.3 + glob: 7.2.3 + minimatch: 3.1.2 + + /text-extensions@2.4.0: + resolution: {integrity: sha512-te/NtwBwfiNRLf9Ijqx3T0nlqZiQ2XrrtBvu+cLL8ZRrGkO0NHTug8MYFKyoSrv/sHTaSKfilUkizV6XhxMJ3g==} + engines: {node: '>=8'} + dev: false + + /text-hex@1.0.0: + resolution: {integrity: sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==} + dev: false + /text-table@0.2.0: resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} - dev: true + + /thenify-all@1.6.0: + resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} + engines: {node: '>=0.8'} + dependencies: + thenify: 3.3.1 + dev: false + + /thenify@3.3.1: + resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} + dependencies: + any-promise: 1.3.0 + dev: false + + /through2@2.0.5: + resolution: {integrity: sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==} + dependencies: + readable-stream: 2.3.8 + xtend: 4.0.2 + dev: false + + /through@2.3.8: + resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==} + dev: false + + /tldts-core@5.7.112: + resolution: {integrity: sha512-mutrEUgG2sp0e/MIAnv9TbSLR0IPbvmAImpzqul5O/HJ2XM1/I1sajchQ/fbj0fPdA31IiuWde8EUhfwyldY1Q==} + dev: false + + /tldts@5.7.87: + resolution: {integrity: sha512-goBoBIXNr5T+lSUG1DYulo1W4sEcglcshzhU1/Dm2eQ0L5FolcycaLsc8hD8TQN9PW93thYCXc0u69AAMjoS3A==} + hasBin: true + dependencies: + tldts-core: 5.7.112 + dev: false + + /tmp@0.0.33: + resolution: {integrity: sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==} + engines: {node: '>=0.6.0'} + dependencies: + os-tmpdir: 1.0.2 + dev: false + + /tmpl@1.0.5: + resolution: {integrity: sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==} /to-fast-properties@2.0.0: resolution: {integrity: sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==} engines: {node: '>=4'} - dev: true /to-regex-range@5.0.1: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} dependencies: is-number: 7.0.0 + + /toidentifier@1.0.1: + resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} + engines: {node: '>=0.6'} + dev: false + + /touch@3.1.0: + resolution: {integrity: sha512-WBx8Uy5TLtOSRtIq+M03/sKDrXCLHxwDcquSP2c43Le03/9serjQBIztjRz6FkJez9D/hleyAXTBGLwwZUw9lA==} + hasBin: true + dependencies: + nopt: 1.0.10 + dev: true + + /tr46@0.0.3: + resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} + dev: false + + /tr46@1.0.1: + resolution: {integrity: sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==} + dependencies: + punycode: 2.3.1 + dev: false + + /traverse@0.6.8: + resolution: {integrity: sha512-aXJDbk6SnumuaZSANd21XAo15ucCDE38H4fkqiGsc3MhCK+wOlZvLP9cB/TvpHT0mOyWgC4Z8EwRlzqYSUzdsA==} + engines: {node: '>= 0.4'} + dev: false + + /tree-kill@1.2.2: + resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} + hasBin: true + dev: false + + /trim-newlines@3.0.1: + resolution: {integrity: sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw==} + engines: {node: '>=8'} + dev: false + + /triple-beam@1.4.1: + resolution: {integrity: sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==} + engines: {node: '>= 14.0.0'} + dev: false + + /ts-api-utils@1.0.3(typescript@5.3.3): + resolution: {integrity: sha512-wNMeqtMz5NtwpT/UZGY5alT+VoKdSsOOP/kqHFcUW1P/VRhH2wJ48+DN2WwUliNbQ976ETwDL0Ifd2VVvgonvg==} + engines: {node: '>=16.13.0'} + peerDependencies: + typescript: '>=4.2.0' + dependencies: + typescript: 5.3.3 + + /ts-interface-checker@0.1.13: + resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} + dev: false + + /ts-invariant@0.10.3: + resolution: {integrity: sha512-uivwYcQaxAucv1CzRp2n/QdYPo4ILf9VXgH19zEIjFx2EJufV16P0JtJVpYHy89DItG6Kwj2oIUjrcK5au+4tQ==} + engines: {node: '>=8'} + dependencies: + tslib: 2.6.2 + dev: false + + /ts-jest@28.0.8(@babel/core@7.23.7)(jest@28.1.3)(typescript@5.3.3): + resolution: {integrity: sha512-5FaG0lXmRPzApix8oFG8RKjAz4ehtm8yMKOTy5HX3fY6W8kmvOrmcY0hKDElW52FJov+clhUbrKAqofnj4mXTg==} + engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} + hasBin: true + peerDependencies: + '@babel/core': '>=7.0.0-beta.0 <8' + '@jest/types': ^28.0.0 + babel-jest: ^28.0.0 + esbuild: '*' + jest: ^28.0.0 + typescript: '>=4.3' + peerDependenciesMeta: + '@babel/core': + optional: true + '@jest/types': + optional: true + babel-jest: + optional: true + esbuild: + optional: true + dependencies: + '@babel/core': 7.23.7 + bs-logger: 0.2.6 + fast-json-stable-stringify: 2.1.0 + jest: 28.1.3 + jest-util: 28.1.3 + json5: 2.2.3 + lodash.memoize: 4.1.2 + make-error: 1.3.6 + semver: 7.5.4 + typescript: 5.3.3 + yargs-parser: 21.1.1 dev: true - /ts-api-utils@1.0.3(typescript@5.3.3): - resolution: {integrity: sha512-wNMeqtMz5NtwpT/UZGY5alT+VoKdSsOOP/kqHFcUW1P/VRhH2wJ48+DN2WwUliNbQ976ETwDL0Ifd2VVvgonvg==} - engines: {node: '>=16.13.0'} + /ts-jest@29.1.1(@babel/core@7.23.7)(jest@29.7.0)(typescript@5.3.3): + resolution: {integrity: sha512-D6xjnnbP17cC85nliwGiL+tpoKN0StpgE0TeOjXQTU6MVCfsB4v7aW05CgQ/1OywGb0x/oy9hHFnN+sczTiRaA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + hasBin: true peerDependencies: - typescript: '>=4.2.0' + '@babel/core': '>=7.0.0-beta.0 <8' + '@jest/types': ^29.0.0 + babel-jest: ^29.0.0 + esbuild: '*' + jest: ^29.0.0 + typescript: '>=4.3 <6' + peerDependenciesMeta: + '@babel/core': + optional: true + '@jest/types': + optional: true + babel-jest: + optional: true + esbuild: + optional: true dependencies: + '@babel/core': 7.23.7 + bs-logger: 0.2.6 + fast-json-stable-stringify: 2.1.0 + jest: 29.7.0(@types/node@20.10.8)(ts-node@10.9.2) + jest-util: 29.7.0 + json5: 2.2.3 + lodash.memoize: 4.1.2 + make-error: 1.3.6 + semver: 7.5.4 typescript: 5.3.3 - dev: true + yargs-parser: 21.1.1 + + /ts-node@10.9.2(@types/node@20.10.8)(typescript@5.3.3): + resolution: {integrity: sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==} + hasBin: true + peerDependencies: + '@swc/core': '>=1.2.50' + '@swc/wasm': '>=1.2.50' + '@types/node': '*' + typescript: '>=2.7' + peerDependenciesMeta: + '@swc/core': + optional: true + '@swc/wasm': + optional: true + dependencies: + '@cspotcode/source-map-support': 0.8.1 + '@tsconfig/node10': 1.0.9 + '@tsconfig/node12': 1.0.11 + '@tsconfig/node14': 1.0.3 + '@tsconfig/node16': 1.0.4 + '@types/node': 20.10.8 + acorn: 8.11.3 + acorn-walk: 8.3.1 + arg: 4.1.3 + create-require: 1.1.1 + diff: 4.0.2 + make-error: 1.3.6 + typescript: 5.3.3 + v8-compile-cache-lib: 3.0.1 + yn: 3.1.1 /tsconfig-paths@3.15.0: resolution: {integrity: sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==} @@ -2966,11 +12871,48 @@ packages: /tslib@1.14.1: resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==} - dev: true /tslib@2.6.2: resolution: {integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==} - dev: true + + /tsup@8.0.1(ts-node@10.9.2)(typescript@5.3.3): + resolution: {integrity: sha512-hvW7gUSG96j53ZTSlT4j/KL0q1Q2l6TqGBFc6/mu/L46IoNWqLLUzLRLP1R8Q7xrJTmkDxxDoojV5uCVs1sVOg==} + engines: {node: '>=18'} + hasBin: true + peerDependencies: + '@microsoft/api-extractor': ^7.36.0 + '@swc/core': ^1 + postcss: ^8.4.12 + typescript: '>=4.5.0' + peerDependenciesMeta: + '@microsoft/api-extractor': + optional: true + '@swc/core': + optional: true + postcss: + optional: true + typescript: + optional: true + dependencies: + bundle-require: 4.0.2(esbuild@0.19.11) + cac: 6.7.14 + chokidar: 3.5.3 + debug: 4.3.4 + esbuild: 0.19.11 + execa: 5.1.1 + globby: 11.1.0 + joycon: 3.1.1 + postcss-load-config: 4.0.2(ts-node@10.9.2) + resolve-from: 5.0.0 + rollup: 4.9.4 + source-map: 0.8.0-beta.0 + sucrase: 3.35.0 + tree-kill: 1.2.2 + typescript: 5.3.3 + transitivePeerDependencies: + - supports-color + - ts-node + dev: false /tsutils@3.21.0(typescript@5.3.3): resolution: {integrity: sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==} @@ -2982,64 +12924,70 @@ packages: typescript: 5.3.3 dev: true - /turbo-darwin-64@1.11.2: - resolution: {integrity: sha512-toFmRG/adriZY3hOps7nYCfqHAS+Ci6xqgX3fbo82kkLpC6OBzcXnleSwuPqjHVAaRNhVoB83L5njcE9Qwi2og==} + /tunnel-agent@0.6.0: + resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==} + dependencies: + safe-buffer: 5.2.1 + dev: false + + /turbo-darwin-64@1.11.3: + resolution: {integrity: sha512-IsOOg2bVbIt3o/X8Ew9fbQp5t1hTHN3fGNQYrPQwMR2W1kIAC6RfbVD4A9OeibPGyEPUpwOH79hZ9ydFH5kifw==} cpu: [x64] os: [darwin] requiresBuild: true dev: true optional: true - /turbo-darwin-arm64@1.11.2: - resolution: {integrity: sha512-FCsEDZ8BUSFYEOSC3rrARQrj7x2VOrmVcfrMUIhexTxproRh4QyMxLfr6LALk4ymx6jbDCxWa6Szal8ckldFbA==} + /turbo-darwin-arm64@1.11.3: + resolution: {integrity: sha512-FsJL7k0SaPbJzI/KCnrf/fi3PgCDCjTliMc/kEFkuWVA6Httc3Q4lxyLIIinz69q6JTx8wzh6yznUMzJRI3+dg==} cpu: [arm64] os: [darwin] requiresBuild: true dev: true optional: true - /turbo-linux-64@1.11.2: - resolution: {integrity: sha512-Vzda/o/QyEske5CxLf0wcu7UUS+7zB90GgHZV4tyN+WZtoouTvbwuvZ3V6b5Wgd3OJ/JwWR0CXDK7Sf4VEMr7A==} + /turbo-linux-64@1.11.3: + resolution: {integrity: sha512-SvW7pvTVRGsqtSkII5w+wriZXvxqkluw5FO/MNAdFw0qmoov+PZ237+37/NgArqE3zVn1GX9P6nUx9VO+xcQAg==} cpu: [x64] os: [linux] requiresBuild: true dev: true optional: true - /turbo-linux-arm64@1.11.2: - resolution: {integrity: sha512-bRLwovQRz0yxDZrM4tQEAYV0fBHEaTzUF0JZ8RG1UmZt/CqtpnUrJpYb1VK8hj1z46z9YehARpYCwQ2K0qU4yw==} + /turbo-linux-arm64@1.11.3: + resolution: {integrity: sha512-YhUfBi1deB3m+3M55X458J6B7RsIS7UtM3P1z13cUIhF+pOt65BgnaSnkHLwETidmhRh8Dl3GelaQGrB3RdCDw==} cpu: [arm64] os: [linux] requiresBuild: true dev: true optional: true - /turbo-windows-64@1.11.2: - resolution: {integrity: sha512-LgTWqkHAKgyVuLYcEPxZVGPInTjjeCnN5KQMdJ4uQZ+xMDROvMFS2rM93iQl4ieDJgidwHCxxCxaU9u8c3d/Kg==} + /turbo-windows-64@1.11.3: + resolution: {integrity: sha512-s+vEnuM2TiZuAUUUpmBHDr6vnNbJgj+5JYfnYmVklYs16kXh+EppafYQOAkcRIMAh7GjV3pLq5/uGqc7seZeHA==} cpu: [x64] os: [win32] requiresBuild: true dev: true optional: true - /turbo-windows-arm64@1.11.2: - resolution: {integrity: sha512-829aVBU7IX0c/B4G7g1VI8KniAGutHhIupkYMgF6xPkYVev2G3MYe6DMS/vsLt9GGM9ulDtdWxWrH5P2ngK8IQ==} + /turbo-windows-arm64@1.11.3: + resolution: {integrity: sha512-ZR5z5Zpc7cASwfdRAV5yNScCZBsgGSbcwiA/u3farCacbPiXsfoWUkz28iyrx21/TRW0bi6dbsB2v17swa8bjw==} cpu: [arm64] os: [win32] requiresBuild: true dev: true optional: true - /turbo@1.11.2: - resolution: {integrity: sha512-jPC7LVQJzebs5gWf8FmEvsvXGNyKbN+O9qpvv98xpNaM59aS0/Irhd0H0KbcqnXfsz7ETlzOC3R+xFWthC4Z8A==} + /turbo@1.11.3: + resolution: {integrity: sha512-RCJOUFcFMQNIGKSjC9YmA5yVP1qtDiBA0Lv9VIgrXraI5Da1liVvl3VJPsoDNIR9eFMyA/aagx1iyj6UWem5hA==} hasBin: true optionalDependencies: - turbo-darwin-64: 1.11.2 - turbo-darwin-arm64: 1.11.2 - turbo-linux-64: 1.11.2 - turbo-linux-arm64: 1.11.2 - turbo-windows-64: 1.11.2 - turbo-windows-arm64: 1.11.2 + turbo-darwin-64: 1.11.3 + turbo-darwin-arm64: 1.11.3 + turbo-linux-64: 1.11.3 + turbo-linux-arm64: 1.11.3 + turbo-windows-64: 1.11.3 + turbo-windows-arm64: 1.11.3 dev: true /type-check@0.4.0: @@ -3047,22 +12995,64 @@ packages: engines: {node: '>= 0.8.0'} dependencies: prelude-ls: 1.2.1 - dev: true + + /type-detect@4.0.8: + resolution: {integrity: sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==} + engines: {node: '>=4'} + + /type-fest@0.12.0: + resolution: {integrity: sha512-53RyidyjvkGpnWPMF9bQgFtWp+Sl8O2Rp13VavmJgfAP9WWG6q6TkrKU8iyJdnwnfgHI6k2hTlgqH4aSdjoTbg==} + engines: {node: '>=10'} + dev: false + + /type-fest@0.18.1: + resolution: {integrity: sha512-OIAYXk8+ISY+qTOwkHtKqzAuxchoMiD9Udx+FSGQDuiRR+PJKJHc2NJAXlbhkGwTt/4/nKZxELY1w3ReWOL8mw==} + engines: {node: '>=10'} + dev: false /type-fest@0.20.2: resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==} engines: {node: '>=10'} - dev: true + + /type-fest@0.21.3: + resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==} + engines: {node: '>=10'} /type-fest@0.6.0: resolution: {integrity: sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==} engines: {node: '>=8'} - dev: true /type-fest@0.8.1: resolution: {integrity: sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==} engines: {node: '>=8'} - dev: true + + /type-fest@1.4.0: + resolution: {integrity: sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==} + engines: {node: '>=10'} + dev: false + + /type-fest@2.19.0: + resolution: {integrity: sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==} + engines: {node: '>=12.20'} + dev: false + + /type-fest@3.13.1: + resolution: {integrity: sha512-tLq3bSNx+xSpwvAJnzrK0Ep5CLNWjvFTOp71URMaAEWBfRb9nnJiBoUe0tF8bI4ZFO3omgBR6NvnbzVUT3Ly4g==} + engines: {node: '>=14.16'} + dev: false + + /type-fest@4.9.0: + resolution: {integrity: sha512-KS/6lh/ynPGiHD/LnAobrEFq3Ad4pBzOlJ1wAnJx9N4EYoqFhMfLIBjUT2UEx4wg5ZE+cC1ob6DCSpppVo+rtg==} + engines: {node: '>=16'} + dev: false + + /type-is@1.6.18: + resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==} + engines: {node: '>= 0.6'} + dependencies: + media-typer: 0.3.0 + mime-types: 2.1.35 + dev: false /typed-array-buffer@1.0.0: resolution: {integrity: sha512-Y8KTSIglk9OZEr8zywiIHG/kmQ7KWyjseXs1CbSo8vC42w7hg2HgYTxSWwP0+is7bWDc1H+Fo026CpHFwm8tkw==} @@ -3102,11 +13092,30 @@ packages: is-typed-array: 1.1.12 dev: true + /typescript@3.9.10: + resolution: {integrity: sha512-w6fIxVE/H1PkLKcCPsFqKE7Kv7QUwhU8qQY2MueZXWx5cPZdwFupLgKK3vntcK98BtNHZtAF4LA/yl2a7k8R6Q==} + engines: {node: '>=4.2.0'} + hasBin: true + dev: false + /typescript@5.3.3: resolution: {integrity: sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==} engines: {node: '>=14.17'} hasBin: true - dev: true + + /typescript@5.4.0-dev.20240110: + resolution: {integrity: sha512-OEtXRprxdta9A5qLObqsgCrFjAWxGuTj8T4W+GBWqDhxIT//BevP5MROHX8Zi18RlvTZSu5G76xJaQT1CK1YpQ==} + engines: {node: '>=14.17'} + hasBin: true + dev: false + + /uglify-js@3.17.4: + resolution: {integrity: sha512-T9q82TJI9e/C1TAxYvfb16xO120tMVFZrGA3f9/P4424DNu6ypK103y0GPFVa17yotwSyZW5iYXgjYHkGrJW/g==} + engines: {node: '>=0.8.0'} + hasBin: true + requiresBuild: true + dev: false + optional: true /unbox-primitive@1.0.2: resolution: {integrity: sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==} @@ -3117,6 +13126,44 @@ packages: which-boxed-primitive: 1.0.2 dev: true + /undefsafe@2.0.5: + resolution: {integrity: sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==} + dev: true + + /undici-types@5.26.5: + resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} + + /unicorn-magic@0.1.0: + resolution: {integrity: sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==} + engines: {node: '>=18'} + dev: false + + /unique-string@3.0.0: + resolution: {integrity: sha512-VGXBUVwxKMBUznyffQweQABPRRW1vHZAbadFZud4pLFAqRGvv/96vafgjWFqzourzr8YonlQiPgH0YCJfawoGQ==} + engines: {node: '>=12'} + dependencies: + crypto-random-string: 4.0.0 + dev: false + + /universal-user-agent@6.0.1: + resolution: {integrity: sha512-yCzhz6FN2wU1NiiQRogkTQszlQSlpWaw8SvVegAc+bDxbzHgh1vX8uIe8OYyMH6DwH+sdTJsgMl36+mSMdRJIQ==} + dev: false + + /universalify@0.1.2: + resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==} + engines: {node: '>= 4.0.0'} + dev: false + + /universalify@2.0.1: + resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} + engines: {node: '>= 10.0.0'} + dev: false + + /unpipe@1.0.0: + resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} + engines: {node: '>= 0.8'} + dev: false + /update-browserslist-db@1.0.13(browserslist@4.22.2): resolution: {integrity: sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==} hasBin: true @@ -3126,20 +13173,101 @@ packages: browserslist: 4.22.2 escalade: 3.1.1 picocolors: 1.0.0 - dev: true /uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} dependencies: punycode: 2.3.1 - dev: true + + /url-join@5.0.0: + resolution: {integrity: sha512-n2huDr9h9yzd6exQVnH/jU5mr+Pfx08LRXXZhkLLetAMESRj+anQsTAh940iMrIetKAmry9coFuZQ2jY8/p3WA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dev: false + + /util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + dev: false + + /utils-merge@1.0.1: + resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} + engines: {node: '>= 0.4.0'} + dev: false + + /uuid@3.4.0: + resolution: {integrity: sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==} + deprecated: Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details. + hasBin: true + dev: false + + /uuid@8.3.2: + resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} + hasBin: true + dev: false + + /uuid@9.0.1: + resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==} + hasBin: true + dev: false + + /v8-compile-cache-lib@3.0.1: + resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==} + + /v8-to-istanbul@9.2.0: + resolution: {integrity: sha512-/EH/sDgxU2eGxajKdwLCDmQ4FWq+kpi3uCmBGpw1xJtnAxEjlD8j8PEiGWpCIMIs3ciNAgH0d3TTJiUkYzyZjA==} + engines: {node: '>=10.12.0'} + dependencies: + '@jridgewell/trace-mapping': 0.3.20 + '@types/istanbul-lib-coverage': 2.0.6 + convert-source-map: 2.0.0 /validate-npm-package-license@3.0.4: resolution: {integrity: sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==} dependencies: spdx-correct: 3.2.0 spdx-expression-parse: 3.0.1 - dev: true + + /value-or-promise@1.0.12: + resolution: {integrity: sha512-Z6Uz+TYwEqE7ZN50gwn+1LCVo9ZVrpxRPOhOLnncYkY1ZzOYtrX8Fwf/rFktZ8R5mJms6EZf5TqNOMeZmnPq9Q==} + engines: {node: '>=12'} + dev: false + + /vary@1.1.2: + resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} + engines: {node: '>= 0.8'} + dev: false + + /walker@1.0.8: + resolution: {integrity: sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==} + dependencies: + makeerror: 1.0.12 + + /webidl-conversions@3.0.1: + resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} + dev: false + + /webidl-conversions@4.0.2: + resolution: {integrity: sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==} + dev: false + + /whatwg-mimetype@3.0.0: + resolution: {integrity: sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==} + engines: {node: '>=12'} + dev: false + + /whatwg-url@5.0.0: + resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} + dependencies: + tr46: 0.0.3 + webidl-conversions: 3.0.1 + dev: false + + /whatwg-url@7.1.0: + resolution: {integrity: sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==} + dependencies: + lodash.sortby: 4.7.0 + tr46: 1.0.1 + webidl-conversions: 4.0.2 + dev: false /which-boxed-primitive@1.0.2: resolution: {integrity: sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==} @@ -3149,7 +13277,6 @@ packages: is-number-object: 1.0.7 is-string: 1.0.7 is-symbol: 1.0.4 - dev: true /which-builtin-type@1.1.3: resolution: {integrity: sha512-YmjsSMDBYsM1CaFiayOVT06+KJeXf0o5M/CAd4o1lTadFAtacTUM49zoYxr/oroopFDfhvN6iEcBxUyc3gvKmw==} @@ -3176,7 +13303,10 @@ packages: is-set: 2.0.2 is-weakmap: 2.0.1 is-weakset: 2.0.2 - dev: true + + /which-module@2.0.1: + resolution: {integrity: sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==} + dev: false /which-typed-array@1.1.13: resolution: {integrity: sha512-P5Nra0qjSncduVPEAr7xhoF5guty49ArDTwzJ/yNuPIbZppyRxFQsRCWrocxIY+CnMVG+qfbU2FmDKyvSGClow==} @@ -3187,7 +13317,6 @@ packages: for-each: 0.3.3 gopd: 1.0.1 has-tostringtag: 1.0.0 - dev: true /which@2.0.2: resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} @@ -3196,19 +13325,241 @@ packages: dependencies: isexe: 2.0.0 + /widest-line@3.1.0: + resolution: {integrity: sha512-NsmoXalsWVDMGupxZ5R08ka9flZjjiLvHVAWYOKtiKM8ujtZWr9cRffak+uSE48+Ob8ObalXpwyeUiyDD6QFgg==} + engines: {node: '>=8'} + dependencies: + string-width: 4.2.3 + dev: false + + /winston-transport@4.6.0: + resolution: {integrity: sha512-wbBA9PbPAHxKiygo7ub7BYRiKxms0tpfU2ljtWzb3SjRjv5yl6Ozuy/TkXf00HTAt+Uylo3gSkNwzc4ME0wiIg==} + engines: {node: '>= 12.0.0'} + dependencies: + logform: 2.6.0 + readable-stream: 3.6.2 + triple-beam: 1.4.1 + dev: false + + /winston@3.11.0: + resolution: {integrity: sha512-L3yR6/MzZAOl0DsysUXHVjOwv8mKZ71TrA/41EIduGpOOV5LQVodqN+QdQ6BS6PJ/RdIshZhq84P/fStEZkk7g==} + engines: {node: '>= 12.0.0'} + dependencies: + '@colors/colors': 1.6.0 + '@dabh/diagnostics': 2.0.3 + async: 3.2.5 + is-stream: 2.0.1 + logform: 2.6.0 + one-time: 1.0.0 + readable-stream: 3.6.2 + safe-stable-stringify: 2.4.3 + stack-trace: 0.0.10 + triple-beam: 1.4.1 + winston-transport: 4.6.0 + dev: false + + /wordwrap@1.0.0: + resolution: {integrity: sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==} + dev: false + + /workerpool@6.5.1: + resolution: {integrity: sha512-Fs4dNYcsdpYSAfVxhnl1L5zTksjvOJxtC5hzMNl+1t9B8hTJTdKDyZ5ju7ztgPy+ft9tBFXoOlDNiOT9WUXZlA==} + dev: false + + /wrap-ansi@6.2.0: + resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} + engines: {node: '>=8'} + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + dev: false + + /wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + /wrap-ansi@8.1.0: + resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} + engines: {node: '>=12'} + dependencies: + ansi-styles: 6.2.1 + string-width: 5.1.2 + strip-ansi: 7.1.0 + dev: false + /wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} - dev: true + + /write-file-atomic@4.0.2: + resolution: {integrity: sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==} + engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + dependencies: + imurmurhash: 0.1.4 + signal-exit: 3.0.7 + + /ws@7.5.9: + resolution: {integrity: sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==} + engines: {node: '>=8.3.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: ^5.0.2 + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + dev: false + + /xml-js@1.6.11: + resolution: {integrity: sha512-7rVi2KMfwfWFl+GpPg6m80IVMWXLRjO+PxTq7V2CDhoGak0wzYzFgUY2m4XJ47OGdXd8eLE8EmwfAmdjw7lC1g==} + hasBin: true + dependencies: + sax: 1.3.0 + dev: false + + /xmlbuilder@15.1.1: + resolution: {integrity: sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==} + engines: {node: '>=8.0'} + dev: false + + /xstate@4.38.3: + resolution: {integrity: sha512-SH7nAaaPQx57dx6qvfcIgqKRXIh4L0A1iYEqim4s1u7c9VoCgzZc+63FY90AKU4ZzOC2cfJzTnpO4zK7fCUzzw==} + dev: false + + /xtend@4.0.2: + resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} + engines: {node: '>=0.4'} + dev: false + + /y18n@4.0.3: + resolution: {integrity: sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==} + dev: false + + /y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} /yallist@3.1.1: resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} - dev: true /yallist@4.0.0: resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} - dev: true + + /yaml@2.3.4: + resolution: {integrity: sha512-8aAvwVUSHpfEqTQ4w/KMlf3HcRdt50E5ODIQJBw1fQ5RL34xabzxtUlzTXVqc4rkZsPbvrXKWnABCD7kWSmocA==} + engines: {node: '>= 14'} + dev: false + + /yargs-parser@18.1.3: + resolution: {integrity: sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==} + engines: {node: '>=6'} + dependencies: + camelcase: 5.3.1 + decamelize: 1.2.0 + dev: false + + /yargs-parser@20.2.9: + resolution: {integrity: sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==} + engines: {node: '>=10'} + dev: false + + /yargs-parser@21.1.1: + resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} + engines: {node: '>=12'} + + /yargs@15.4.1: + resolution: {integrity: sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==} + engines: {node: '>=8'} + dependencies: + cliui: 6.0.0 + decamelize: 1.2.0 + find-up: 4.1.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + require-main-filename: 2.0.0 + set-blocking: 2.0.0 + string-width: 4.2.3 + which-module: 2.0.1 + y18n: 4.0.3 + yargs-parser: 18.1.3 + dev: false + + /yargs@16.2.0: + resolution: {integrity: sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==} + engines: {node: '>=10'} + dependencies: + cliui: 7.0.4 + escalade: 3.1.1 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 20.2.9 + dev: false + + /yargs@17.7.2: + resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} + engines: {node: '>=12'} + dependencies: + cliui: 8.0.1 + escalade: 3.1.1 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 21.1.1 + + /yauzl@2.10.0: + resolution: {integrity: sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==} + dependencies: + buffer-crc32: 0.2.13 + fd-slicer: 1.1.0 + dev: false + + /yn@3.1.1: + resolution: {integrity: sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==} + engines: {node: '>=6'} /yocto-queue@0.1.0: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} - dev: true + + /yocto-queue@1.0.0: + resolution: {integrity: sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==} + engines: {node: '>=12.20'} + dev: false + + /yoga-layout-prebuilt@1.10.0: + resolution: {integrity: sha512-YnOmtSbv4MTf7RGJMK0FvZ+KD8OEe/J5BNnR0GHhD8J/XcG/Qvxgszm0Un6FTHWW4uHlTgP0IztiXQnGyIR45g==} + engines: {node: '>=8'} + dependencies: + '@types/yoga-layout': 1.9.2 + dev: false + + /zen-observable-ts@1.2.5: + resolution: {integrity: sha512-QZWQekv6iB72Naeake9hS1KxHlotfRpe+WGNbNx5/ta+R3DNjVO2bswf63gXlWDcs+EMd7XY8HfVQyP1X6T4Zg==} + dependencies: + zen-observable: 0.8.15 + dev: false + + /zen-observable@0.8.15: + resolution: {integrity: sha512-PQ2PC7R9rslx84ndNBZB/Dkv8V8fZEpk83RLgXtYd0fwUgEjseMn1Dgajh2x6S8QbZAFa9p2qVCEuYZNgve0dQ==} + dev: false + + /zip-stream@4.1.1: + resolution: {integrity: sha512-9qv4rlDiopXg4E69k+vMHjNN63YFMe9sZMrdlvKnCjlCRWeCBswPPMPUfx+ipsAWq1LXHe70RcbaHdJJpS6hyQ==} + engines: {node: '>= 10'} + dependencies: + archiver-utils: 3.0.4 + compress-commons: 4.1.2 + readable-stream: 3.6.2 + dev: false + + /zod@3.22.4: + resolution: {integrity: sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==} + dev: false diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index f8b03a11..00ac3a91 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,7 +1,7 @@ packages: # all apps in direct subdirs of apps/ - "apps/*" - - "packages/*" - "servers/*" - - ".aws/*" + - "packages/*" + - "infrastructure/*" - "lambdas/*" \ No newline at end of file diff --git a/servers/prospect-api/.eslintrc.js b/servers/prospect-api/.eslintrc.js new file mode 100644 index 00000000..fa01401d --- /dev/null +++ b/servers/prospect-api/.eslintrc.js @@ -0,0 +1,3 @@ +module.exports = { + extends: ['custom/graphql'], +}; \ No newline at end of file diff --git a/servers/prospect-api/README.md b/servers/prospect-api/README.md new file mode 100644 index 00000000..26ebe69f --- /dev/null +++ b/servers/prospect-api/README.md @@ -0,0 +1,159 @@ +# Prospect API + +This repo contains two distinct but related applications: an API and a Lambda. + +The API is used by our curation admin tools to retrieve ML generated prospects for curator review. + +The Lambda processes ML prospects from an SQS queue and inserts them into a DynamoDB table. This DynamoDB table is the data source for the API. + +These two distinct applications share code - specifically DynamoDB interactions, TypeScript types, and utility functions. This shared code resides in the `prospectapi-common` folder, and is used as a local NPM package. (More details on how this package is used/installed below.) + +This repo leverages [Pocket's share data structures](https://getpocket.atlassian.net/wiki/spaces/PE/pages/2584150049/Pocket+Shared+Data). + +## API + +The API code resides in the `/src` folder. + +### Application Overview + +[Express](https://expressjs.com/) is the Node framework, [Apollo Server](https://www.apollographql.com/docs/apollo-server/) is used in Express to expose a GraphQL API, and data is retrieved from [DynamoDB](https://aws.amazon.com/dynamodb/) via a thin, custom wrapper around the AWS SDK. + +The API provides one query to retrieve prospects (which can accept an optional set of filters), and one mutation to mark a prospect as curated (so it doesn't get sent to another curator for review). + +### GraphQL Schemas + +This application has a single, non-federated GraphQL schema. This schema may become federated if/when there is a more broad use case for retrieving prospects. + +## Lambda + +The Lambda code resides in the `/aws_lambda` folder. + +### Application Overview + +The Lambda is a Node application that is triggered off an SQS queue. Messages are sent to the SQS queue at regular intervals by ML Metaflow jobs (one job per `ProspectType`/`ScheduledSurface` combo). When the queue gets a message, the Lambda is executed, and performs the general steps below, in order: + +1. Validate the format of the SQS message +2. Validate the format of each prospect contained in the SQS message +3. Delete all old prospects matching the `ProspectType` and `ScheduledSurface` of the current SQS message - this is how prospects are refreshed and not duplicated in DynamoDB +4. Hydrate each prospect with meta data from Client API (specifically, the Parser) +5. Insert each prospect into DynamoDB + +## Common Module + +The common module code is in the `prospectapi-common` folder. + +### Module Overview + +The shared code exists as its own NPM package to be installed and used by the API and the Lambda. The code is shared via `npm link`, which places this module in the global `npm_modules` folder (which is system dependent in location). For local development, a symlink is created in both the API and Lambda directories. Because symlinking doesn't play well out in the wild, during deployments we copy this package from the global `npm_modules` folder into the application specific `npm_modules` folder. + +## Local Development + +### API + +Clone the repo: + +- `git clone git@github.com:Pocket/prospect-api.git` +- `cd prospect-api` + +Start the Docker containers: + +- `docker compose up` + +After Docker completes, you should be able to hit the GraphQL playground at `http://localhost:4026`. + +Note the app startup command in `/docker-compose.yml`: + +```bash +bash -c 'cd /app && +cd prospectapi-common && +npm ci && +npm run build && +npm link && +cd ../aws_lambda && +npm ci && +npm link prospectapi-common && +cd .. && +npm ci && +npm link prospectapi-common && +npm run start:dev' +``` + +Using `npm link`, this "installs" the common module into the API and the Lambda. + +#### Seeding Your Local Database + +To seed your local DynamoDB, run the following: + +`docker compose exec app npm run seed` + +Note: you'll need to re-seed your database after running integration tests! + +#### API Authorization + +The API requires HTTP headers be set to authorize operations (both queries and mutations). + +To run operations in the GraphQL playground, you'll need to specify some HTTP headers. To do so: + +1. Open up the GraphQL playground at `http://localhost:4026`. +2. Click the **HTTP HEADERS** link at the bottom of the left hand side of the playground to reveal a text box. +3. Enter the necessary headers (see sample below) into the box and try an operation - it should work! + +The sample headers below allow full access to all queries and mutations: + +```typescript +{ + "groups": "mozilliansorg_pocket_scheduled_surface_curator_full", + "name": "Cherry Glazer", + "username": "ad|Mozilla-LDAP|cglazer" +} +``` + +Note that the `groups` header can contain mulitple values separated by commas (but still in a single string). + +If you'd like to experiment with different levels of authorization (e.g. access to only one scheduled surface), you can find the full list of Mozillian groups on our [Shared Data document](https://getpocket.atlassian.net/wiki/spaces/PE/pages/2584150049/Pocket+Shared+Data#Source-of-Truth.3). + +### Common Module + +Changes made to the common module _should_ be picked up automatically via the `node_modules` symlink. Howevever, this has been spotty in practice, so if you aren't seeing a change made in the common module reflected in either the API or the Lambda, you can try the following: + +1. `cd prospectapi-common` +2. make your changes +3. `npm run build` +4. `cd ../src && npm link prospectapi-common` +5. `cd ../aws_lambda && npm link prospectapi-common` + +You may need to repeat the steps above every time you make changes to the common module. If you have to perform the steps above, be noisy about it in Slack. If this becomes enough of an issue, we should devote time to fixing it. + +## Running Tests + +We have two test commands for each component (API, Lambda, Common Module): one for unit/functional tests and one for integration tests. These are both run by [Jest](https://jestjs.io/) and are differentiated by file names. Any file ending with `.spec.ts` will be run in the unit/functional suite, while integration tests should have the `.integration.ts` suffix. + +Unit/functional tests are self-contained, meaning they do not rely on any external services. Integration tests rely on MySQL and AWS (which is mocked by a [localstack](https://github.com/localstack/localstack) Docker container locally). + +Test are run via `npm` commands: + +- Unit/functional: + +```bash +npm test +``` + +- Integration: + +```bash +npm run test-integrations +``` + +As each component in this repo (API, Lambda, Common Module) have different dependencies, they need to be tested separately (otherwise the root `package.json` would need to contain dependencies for all components). + +### API + +To test the API, run the above commands from the project root. + +### Lambda + +To test the Lambda, run the above commands in the `aws_lambda` folder. + +### Common Module + +To test the common module, run the above commands in the `prospectapi-common` folder. diff --git a/servers/prospect-api/jest.config.js b/servers/prospect-api/jest.config.js new file mode 100644 index 00000000..f7892883 --- /dev/null +++ b/servers/prospect-api/jest.config.js @@ -0,0 +1,10 @@ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', + testMatch: ['**/?(*.)+(spec|integration).ts'], + // ignore aws_lambda & prospectapi-common - those modules run their own + // tests independently + testPathIgnorePatterns: ['/dist/', '/aws_lambda/', '/prospectapi-common/'], + setupFiles: ['./jest.setup.js'], + verbose: true, +}; diff --git a/servers/prospect-api/jest.setup.js b/servers/prospect-api/jest.setup.js new file mode 100644 index 00000000..d8328cc0 --- /dev/null +++ b/servers/prospect-api/jest.setup.js @@ -0,0 +1,4 @@ +// AWS_ENDPOINT is set in .docker/local.env +// this file is to provide a value when running tests outside of the container +process.env.AWS_ENDPOINT = process.env.AWS_ENDPOINT || 'http://localhost:4566'; +jest.setTimeout(8000); diff --git a/servers/prospect-api/package.json b/servers/prospect-api/package.json new file mode 100644 index 00000000..e9b8df07 --- /dev/null +++ b/servers/prospect-api/package.json @@ -0,0 +1,52 @@ +{ + "name": "prospect-api", + "version": "1.0.0", + "description": "", + "main": "dist/main.js", + "scripts": { + "build": "rm -rf dist && tsc", + "watch": "tsc -w & nodemon", + "start": "node dist/main.js", + "dev": "npm run build && npm run watch", + "test-ci": "npm test", + "test:watch": "npm test -- --watchAll", + "test": "jest \"\\.spec\\.ts\"", + "test-integrations": "jest \"\\.integration\\.ts\" --runInBand", + "lint-check": "eslint --fix-dry-run \"src/**/*.ts\"", + "lint-fix": "eslint --fix \"src/**/*.ts\"", + "db:seed": "node dist/seeder.js" + }, + "author": "", + "license": "ISC", + "dependencies": { + "@aws-sdk/client-dynamodb": "3.391.0", + "@aws-sdk/client-eventbridge": "3.342.0", + "@aws-sdk/lib-dynamodb": "3.391.0", + "@pocket-tools/apollo-utils": "3.0.0", + "@pocket-tools/ts-logger": "^1.2.4", + "@snowplow/node-tracker": "^3.5.0", + "prospectapi-common": "workspace:*", + "supertest": "^6.3.3" + }, + "devDependencies": { + "@faker-js/faker": "6.0.0", + "eslint-config-custom": "workspace:*", + "tsconfig": "workspace:*", + "@types/chai": "4.3.4", + "@types/chai-spies": "1.0.3", + "@types/jest": "28.1.7", + "@types/lodash": "4.14.182", + "chai": "4.3.7", + "chai-spies": "1.0.0", + "jest": "29.7.0", + "lodash": "4.17.21", + "nodemon": "2.0.22", + "sinon": "^17.0.1", + "ts-jest": "29.1.1" + }, + "files": [ + "dist", + "schema.graphql", + "package.json" + ] +} diff --git a/servers/prospect-api/schema.graphql b/servers/prospect-api/schema.graphql new file mode 100644 index 00000000..dcc2a801 --- /dev/null +++ b/servers/prospect-api/schema.graphql @@ -0,0 +1,120 @@ +enum CorpusLanguage { + "German" + DE + "English" + EN + "Italian" + IT + "French" + FR + "Spanish" + ES +} + +""" +A URL - usually, for an interesting story on the internet that's worth saving to Pocket. +""" +scalar Url + +""" +Resolve by reference the Item entity to connect Prospect to Items. +""" +type Item @key(fields: "givenUrl", resolvable: false) { + givenUrl: Url! +} + +type Prospect { + id: ID! + prospectId: ID! + scheduledSurfaceGuid: String! + topic: String + prospectType: String! + url: String! + createdAt: Int + imageUrl: String + publisher: String + domain: String + title: String + excerpt: String + language: CorpusLanguage + saveCount: Int + isSyndicated: Boolean + isCollection: Boolean + authors: String + approvedCorpusItem: ApprovedCorpusItem + rejectedCorpusItem: RejectedCorpusItem + item: Item +} + +extend type ApprovedCorpusItem @key(fields: "url") { + "key field to identify the Approved Corpus Item entity in the Curated Corpus service" + url: Url! @external +} + +extend type RejectedCorpusItem @key(fields: "url") { + "key field to identify the Rejected Corpus Item entity in the Curated Corpus service" + url: Url! @external +} + +type UrlMetadata { + url: String! + imageUrl: String + publisher: String + domain: String + title: String + excerpt: String + language: String + isSyndicated: Boolean + isCollection: Boolean + authors: String +} + +input GetProspectsFilters { + """ + string GUID of the scheduled surface being prospected, e.g. 'NEW_TAB_EN_US' or 'POCKET_HITS_DE_DE' + """ + scheduledSurfaceGuid: String! + """ + string GUID of the prospect type to further filter prospects, e.g. 'GLOBAL' or 'ORGANIC_TIMESPENT' + """ + prospectType: String + """ + Filter the returned prospects by the name or part-match of the name of a publisher, e.g. 'The Onion'. + Note that this filter is case-sensitive due to DynamoDB limitations. + """ + includePublisher: String + """ + Filter out any prospects by the name or part-match of the name of a publisher, e.g. 'The Onion'. + Note that this filter is case-sensitive due to DynamoDB limitations. + """ + excludePublisher: String +} + +type Query { + """ + returns a set of at most 20 prospects (number may be smaller depending on available data) + """ + getProspects(filters: GetProspectsFilters!): [Prospect!]! + + """ + returns parser meta data for a given url + """ + getUrlMetadata(url: String!): UrlMetadata! +} + +type Mutation { + """ + Marks a prospect as 'curated' in the database, preventing it from being displayed for prospecting. + Returns the prospect if the operation succeeds, and null if not (almost surely due to an incorrect id). + """ + updateProspectAsCurated(id: ID!): Prospect + + """ + Marks a prospect as 'curated' in the database, preventing it from being displayed for prospecting. + Returns the prospect if the operation succeeds, and null if not (almost surely due to an incorrect id). + + This is functionally a copy of updateProspectAsCurated for now, but will have differences in + snowplow reporting down the line. + """ + dismissProspect(id: ID!): Prospect +} diff --git a/servers/prospect-api/src/aws/dynamodb/lib.integration.ts b/servers/prospect-api/src/aws/dynamodb/lib.integration.ts new file mode 100644 index 00000000..baba761f --- /dev/null +++ b/servers/prospect-api/src/aws/dynamodb/lib.integration.ts @@ -0,0 +1,748 @@ +import { expect } from 'chai'; +import request from 'supertest'; +import { print } from 'graphql'; + +import { + dbClient, + Prospect, + ProspectType, + insertProspect, + getProspectById, + truncateDb, + createProspect, +} from 'prospectapi-common'; + +import config from '../../config'; + +import { getTestServer } from '../../test/admin-server'; +import { GET_PROSPECTS } from '../../test/admin-server/queries.gql'; +import { + UPDATE_DISMISS_PROSPECT, + UPDATE_PROSPECT_AS_CURATED, +} from '../../test/admin-server/mutations.gql'; +import { Context, MozillaAccessGroup } from '../../types'; +import { ApolloServer } from '@apollo/server'; + +// These tests randomly break with "Exceeded timeout of 1500 ms for a hook" both locally +// and in CircleCI. Increasing timeout value +jest.setTimeout(30 * 1000); +jest.useRealTimers(); + +// convenience function to seed db +const seedDb = async () => { + // plenty of NEW_TAB_EN_US / COUNTS not curated + let prospect: Prospect; + + for (let i = 0; i < config.app.prospectBatchSize * 2; i++) { + await insertProspect( + dbClient, + createProspect('NEW_TAB_EN_US', ProspectType.COUNTS) + ); + } + + // half-batch of NEW_TAB_EN_US / TIMESPENT not curated + for (let i = 0; i < config.app.prospectBatchSize / 2; i++) { + prospect = createProspect('NEW_TAB_EN_US', ProspectType.TIMESPENT); + + // purposefully omit some optional data so we can verify this returns + // as expected below + delete prospect.excerpt; + delete prospect.language; + delete prospect.authors; + + await insertProspect(dbClient, prospect); + } + + // plenty of NEW_TAB_DE_DE / GOBAL not curated + for (let i = 0; i < config.app.prospectBatchSize * 2; i++) { + await insertProspect( + dbClient, + createProspect('NEW_TAB_DE_DE', ProspectType.COUNTS) + ); + } + + // NEW_TAB_EN_US / COUNTS curated - these should not be returned at all + for (let i = 0; i < config.app.prospectBatchSize * 2; i++) { + await insertProspect( + dbClient, + createProspect('NEW_TAB_EN_US', ProspectType.COUNTS, true) + ); + } +}; +describe('queries integration tests', () => { + // we assign the auth groups that provide full access + const headers = { + name: 'test-user', + username: 'test-user-name', + groups: [ + MozillaAccessGroup.READONLY, + MozillaAccessGroup.SCHEDULED_SURFACE_CURATOR_FULL, + ].join(), + }; + + let app: Express.Application; + let apolloServer: ApolloServer; + let url: string; + + beforeAll(async () => { + await seedDb(); + ({ app, apolloServer, url } = await getTestServer(0)); + }); + + afterAll(async () => { + await truncateDb(dbClient); + dbClient.destroy(); + await apolloServer.stop(); + }); + + describe('getProspects', () => { + it('should return all prospect properties', async () => { + // this will give us 10 prospects + const result = await request(app) + .post(url) + .set(headers) + .send({ + query: print(GET_PROSPECTS), + variables: { + filters: { + scheduledSurfaceGuid: 'NEW_TAB_EN_US', + prospectType: ProspectType.COUNTS, + }, + }, + }); + expect(result.body.errors).to.be.undefined; + const { + body: { data }, + } = result; + const resultArray = data?.getProspects; + resultArray.forEach((item: Prospect) => { + expect(item.id).to.exist; + expect(item.scheduledSurfaceGuid).to.exist; + expect(item.url).to.exist; + expect(item.topic).to.exist; + expect(item.prospectType).to.exist; + expect(item.saveCount).to.exist; + expect(item.createdAt).to.exist; + expect(item.domain).to.exist; + expect(item.imageUrl).to.exist; + expect(item.publisher).to.exist; + expect(item.title).to.exist; + expect(item.isSyndicated).to.exist; + expect(item.isCollection).to.exist; + expect(item.excerpt).to.exist; + expect(item.language).to.exist; + expect(item.authors).to.exist; + // Federated Approved Corpus Item + expect(item.approvedCorpusItem).to.exist; + // `url` is the only field that is resolved locally + expect(item.approvedCorpusItem?.url).to.exist; + }); + }); + + it('should return all prospect properties, even undefined ones', async () => { + // this will give us 10 prospects, some of which are missing optional + // attributes - language, excerpt, and authors + const result = await request(app) + .post(url) + .set(headers) + .send({ + query: print(GET_PROSPECTS), + variables: { + filters: { + scheduledSurfaceGuid: 'NEW_TAB_EN_US', + prospectType: ProspectType.TIMESPENT, + }, + }, + }); + expect(result.body.errors).to.be.undefined; + const { + body: { data }, + } = result; + + const resultArray = data?.getProspects; + + resultArray.forEach((item: Prospect) => { + expect(item.id).to.exist; + expect(item.scheduledSurfaceGuid).to.exist; + expect(item.url).to.exist; + expect(item.topic).to.exist; + expect(item.prospectType).to.exist; + expect(item.saveCount).to.exist; + expect(item.createdAt).to.exist; + expect(item.domain).to.exist; + expect(item.imageUrl).to.exist; + expect(item.publisher).to.exist; + expect(item.title).to.exist; + expect(item.isSyndicated).to.exist; + expect(item.isCollection).to.exist; + // Federated Approved Corpus Item + expect(item.approvedCorpusItem).to.exist; + // `url` is the only field that is resolved locally + expect(item.approvedCorpusItem?.url).to.exist; + + // these are purposefully removed in seedDb() above + expect(item.excerpt).not.to.exist; + expect(item.language).not.to.exist; + expect(item.authors).not.to.exist; + }); + }); + + it('should return a full batch filtered by new tab', async () => { + const result = await request(app) + .post(url) + .set(headers) + .send({ + query: print(GET_PROSPECTS), + variables: { + filters: { + scheduledSurfaceGuid: 'NEW_TAB_EN_US', + }, + }, + }); + + expect(result.body.errors).to.be.undefined; + const { + body: { data }, + } = result; + const resultArray = data?.getProspects; + + // all prospects should be NEW_TAB_EN_US + const enUsCount = resultArray.reduce((counter, result) => { + return ( + counter + (result.scheduledSurfaceGuid === 'NEW_TAB_EN_US' ? 1 : 0) + ); + }, 0); + + expect(enUsCount).to.equal(resultArray.length); + }); + + it('should return a full batch filtered by new tab and prospect type', async () => { + const result = await request(app) + .post(url) + .set(headers) + .send({ + query: print(GET_PROSPECTS), + variables: { + filters: { + scheduledSurfaceGuid: 'NEW_TAB_EN_US', + prospectType: ProspectType.COUNTS, + }, + }, + }); + expect(result.body.errors).to.be.undefined; + const { + body: { data }, + } = result; + + const resultArray = data?.getProspects; + + // all prospects should be NEW_TAB_EN_US/COUNTS + const validCount = resultArray.reduce((counter, result) => { + if ( + result.scheduledSurfaceGuid === 'NEW_TAB_EN_US' && + result.prospectType === ProspectType.COUNTS + ) { + return counter + 1; + } else { + return counter; + } + }, 0); + + expect(validCount).to.equal(resultArray.length); + }); + + it('should return a partial batch filtered by publisher (included)', async () => { + const result = await request(app) + .post(url) + .set(headers) + .send({ + query: print(GET_PROSPECTS), + variables: { + filters: { + scheduledSurfaceGuid: 'NEW_TAB_EN_US', + // A partial but case-sensitive match for The Atlantic + includePublisher: 'Atlant', + }, + }, + }); + + // check these first just in case + expect(result.body.errors).to.be.undefined; + expect(result.body.data).not.to.be.null; + + const resultArray = result.body.data?.getProspects; + + // all returned prospects should be from The Atlantic + const resultCount = resultArray.reduce( + (counter, current) => + current.publisher === 'The Atlantic' ? ++counter : counter, + 0 + ); + + expect(resultCount).to.equal(resultArray.length); + }); + + it('should return a partial batch filtered by publisher (excluded)', async () => { + const result = await request(app) + .post(url) + .set(headers) + .send({ + query: print(GET_PROSPECTS), + variables: { + filters: { + scheduledSurfaceGuid: 'NEW_TAB_EN_US', + // A partial but case-sensitive match for The New York Times + excludePublisher: 'New', + }, + }, + }); + expect(result.body.errors).to.be.undefined; + const { + body: { data }, + } = result; + + const resultArray = data?.getProspects; + + // all returned prospects should be from The Atlantic + const resultCount = resultArray.reduce( + (counter, current) => + current.publisher !== 'The New York Times' ? ++counter : counter, + 0 + ); + + expect(resultCount).to.equal(resultArray.length); + }); + + it('should return less than a full batch when limited items exist', async () => { + const result = await request(app) + .post(url) + .set(headers) + .send({ + query: print(GET_PROSPECTS), + variables: { + filters: { + scheduledSurfaceGuid: 'NEW_TAB_EN_US', + prospectType: ProspectType.TIMESPENT, + }, + }, + }); + + expect(result.body.errors).to.be.undefined; + const { + body: { data }, + } = result; + + const resultArray = data?.getProspects; + + // all prospects should be NEW_TAB_EN_US/TIMESPENT + const validCount = resultArray.reduce((counter, result) => { + if ( + result.scheduledSurfaceGuid === 'NEW_TAB_EN_US' && + result.prospectType === ProspectType.TIMESPENT + ) { + return counter + 1; + } else { + return counter; + } + }, 0); + + expect(validCount).to.equal(resultArray.length); + }); + + it('should throw an error if given an invalid new tab', async () => { + const result = await request(app) + .post(url) + .set(headers) + .send({ + query: print(GET_PROSPECTS), + variables: { + filters: { + scheduledSurfaceGuid: 'NEW_TAB_CY_GB', // Welsh - probably safe for the foreseeable future + }, + }, + }); + + expect(result.body.errors?.length).to.equal(1); + + // this *should* always be true - but I couldn't figure out how to access + // a specific index of a possibly undefined array + if (result.body.errors) { + expect(result.body.errors[0].message).to.equal( + "NEW_TAB_CY_GB isn't a valid scheduled surface guid!" + ); + } + }); + + it('should throw an error if given an invalid prospect type', async () => { + const result = await request(app) + .post(url) + .set(headers) + .send({ + query: print(GET_PROSPECTS), + variables: { + filters: { + scheduledSurfaceGuid: 'NEW_TAB_DE_DE', + prospectType: ProspectType.SYNDICATED_NEW, + }, + }, + }); + + // we should get an error + expect(result.body.errors?.length).to.equal(1); + + // this *should* always be true - but i couldn't figure out how to access + // a specific index of a possibly undefined array + + expect(result?.body.errors?.[0].message).to.equal( + 'SYNDICATED_NEW is not a valid prospect type for scheduled surface New Tab (de-DE)' + ); + }); + it('should throw an error if the user does not have the required auth group for a given scheduled surface', async () => { + // we assign auth group for the DE_DE scheduled surface + const unAuthorizedHeaders = { + ...headers, + groups: MozillaAccessGroup.NEW_TAB_CURATOR_DEDE, + }; + + // the request we make is for EN_US scheduled surface + const result = await request(app) + .post(url) + .set(unAuthorizedHeaders) + .send({ + query: print(GET_PROSPECTS), + variables: { + filters: { + scheduledSurfaceGuid: 'NEW_TAB_EN_US', + prospectType: ProspectType.SYNDICATED_NEW, + }, + }, + }); + + // we should get an authorization error + expect(result.body.errors).to.not.be.undefined; + + expect(result.body.errors?.length).to.equal(1); + + expect(result?.body.errors?.[0].message).to.equal( + 'Not authorized for action' + ); + }); + it('should not return any prospects if request header groups are missing', async () => { + // destructure groups to remove it from baseHeaders + const { groups, ...undefinedGroupsHeaders } = { + ...headers, + }; + expect(groups).to.exist; + + // the request we make is for EN_US scheduled surface + const result = await request(app) + .post(url) + .set(undefinedGroupsHeaders) + .send({ + query: print(GET_PROSPECTS), + variables: { + filters: { + scheduledSurfaceGuid: 'NEW_TAB_EN_US', + }, + }, + }); + // we should get an authorization error + expect(result.body.errors).to.not.be.undefined; + + expect(result.body.data).to.be.null; + + expect(result.body.errors?.length).to.equal(1); + + expect(result?.body.errors?.[0].message).to.equal( + 'Not authorized for action' + ); + }); + }); +}); + +describe('mutations integration tests', () => { + const headers = { + name: 'test-user', + username: 'test-user-name', + groups: [ + MozillaAccessGroup.READONLY, + MozillaAccessGroup.SCHEDULED_SURFACE_CURATOR_FULL, + ].join(), + }; + + let app: Express.Application; + let apolloServer: ApolloServer; + let url: string; + + beforeAll(async () => { + jest.setTimeout(15 * 1000); + await truncateDb(dbClient); + ({ app, apolloServer, url } = await getTestServer(0)); + }); + + afterAll(async () => { + await dbClient.destroy(); + await apolloServer.stop(); + }); + + afterEach(async () => { + await truncateDb(dbClient); + }); + + describe('updateProspectAsCurated', () => { + it('should updated a prospect as curated', async () => { + const prospect = createProspect( + 'NEW_TAB_EN_US', + ProspectType.SYNDICATED_NEW, + false + ); + + await insertProspect(dbClient, prospect); + + const result = await request(app) + .post(url) + .set(headers) + .send({ + query: print(UPDATE_PROSPECT_AS_CURATED), + variables: { + id: prospect.id, + }, + }); + + // check these first just in case + expect(result.body.errors).to.be.undefined; + expect(result.body.data).not.to.be.null; + + // get the prospect directly from the db (as `curated` is not a part of + // our graph) + const res = await getProspectById(dbClient, prospect.id); + + expect(res?.curated).to.equal(true); + }); + + it('should return all properties of an updated prospect', async () => { + const prospect = createProspect( + 'NEW_TAB_EN_US', + ProspectType.SYNDICATED_NEW, + false + ); + + await insertProspect(dbClient, prospect); + + const result = await request(app) + .post(url) + .set(headers) + .send({ + query: print(UPDATE_PROSPECT_AS_CURATED), + variables: { + id: prospect.id, + }, + }); + + // check these first just in case + expect(result.body.errors).to.be.undefined; + expect(result.body.data).not.to.be.null; + + const updatedProspect = result.body.data?.updateProspectAsCurated; + + // internal Prospect type differs from graph Prospect type + // curated and rank are not exposed via public graph, redact from expectations + const expectedProspect: any = { ...prospect }; + delete expectedProspect.curated; + delete expectedProspect.rank; + // add expected federated fields that are on graphql schema + expectedProspect.approvedCorpusItem = { url: expectedProspect.url }; + + expect(updatedProspect).to.deep.equal(expectedProspect); + }); + + it('should update prospect if the user has the required auth group for a given scheduled surface', async () => { + const prospect = createProspect( + 'NEW_TAB_EN_US', + ProspectType.SYNDICATED_NEW, + false + ); + + await insertProspect(dbClient, prospect); + + const result = await request(app) + .post(url) + .set(headers) + .send({ + query: print(UPDATE_PROSPECT_AS_CURATED), + variables: { + id: prospect.id, + }, + }); + + expect(result.body.errors).to.be.undefined; + + expect(result.body.data).not.to.be.null; + + // get the prospect directly from the db (as `curated` is not a part of + // our graph) + const res = await getProspectById(dbClient, prospect.id); + + expect(res?.curated).to.equal(true); + }); + it('should throw an error if the user does not have the required auth group for a given scheduled surface', async () => { + const prospect = createProspect( + 'NEW_TAB_EN_US', + ProspectType.SYNDICATED_NEW, + false + ); + + await insertProspect(dbClient, prospect); + + const unAuthorizedHeaders = { + ...headers, + groups: MozillaAccessGroup.NEW_TAB_CURATOR_DEDE, + }; + + const result = await request(app) + .post(url) + .set(unAuthorizedHeaders) + .send({ + query: print(UPDATE_PROSPECT_AS_CURATED), + variables: { + id: prospect.id, + }, + }); + + expect(result.body.errors).not.to.be.undefined; + + expect(result.body.errors?.length).to.equal(1); + + expect(result?.body.errors?.[0].message).to.equal( + 'Not authorized for action' + ); + }); + }); + describe('dismissProspect', () => { + it('should updated a prospect as curated', async () => { + const prospect = createProspect( + 'NEW_TAB_EN_US', + ProspectType.SYNDICATED_NEW, + false + ); + + await insertProspect(dbClient, prospect); + + const result = await request(app) + .post(url) + .set(headers) + .send({ + query: print(UPDATE_DISMISS_PROSPECT), + variables: { + id: prospect.id, + }, + }); + + // check these first just in case + expect(result.body.errors).to.be.undefined; + expect(result.body.data).not.to.be.null; + + // get the prospect directly from the db (as `curated` is not a part of + // our graph) + const res = await getProspectById(dbClient, prospect.id); + + expect(res?.curated).to.equal(true); + }); + + it('should return all properties of an updated prospect', async () => { + const prospect = createProspect( + 'NEW_TAB_EN_US', + ProspectType.SYNDICATED_NEW, + false + ); + + await insertProspect(dbClient, prospect); + + const result = await request(app) + .post(url) + .set(headers) + .send({ + query: print(UPDATE_DISMISS_PROSPECT), + variables: { + id: prospect.id, + }, + }); + + // check these first just in case + expect(result.body.errors).to.be.undefined; + expect(result.body.data).not.to.be.null; + + const updatedProspect = result.body.data?.dismissProspect; + + // internal Prospect type differs from graph Prospect type + // curated and rank are not exposed via public graph, redact from expectations + const expectedProspect: any = { ...prospect }; + delete expectedProspect.curated; + delete expectedProspect.rank; + // add expected federated fields that are on graphql schema + expectedProspect.approvedCorpusItem = { url: expectedProspect.url }; + + expect(updatedProspect).to.deep.equal(expectedProspect); + }); + + it('should update prospect if the user has the required auth group for a given scheduled surface', async () => { + const prospect = createProspect( + 'NEW_TAB_EN_US', + ProspectType.SYNDICATED_NEW, + false + ); + + await insertProspect(dbClient, prospect); + + const result = await request(app) + .post(url) + .set(headers) + .send({ + query: print(UPDATE_DISMISS_PROSPECT), + variables: { + id: prospect.id, + }, + }); + + expect(result.body.errors).to.be.undefined; + + expect(result.body.data).not.to.be.null; + + // get the prospect directly from the db (as `curated` is not a part of + // our graph) + const res = await getProspectById(dbClient, prospect.id); + + expect(res?.curated).to.equal(true); + }); + it('should throw an error if the user does not have the required auth group for a given scheduled surface', async () => { + const prospect = createProspect( + 'NEW_TAB_EN_US', + ProspectType.SYNDICATED_NEW, + false + ); + + await insertProspect(dbClient, prospect); + + const unAuthorizedHeaders = { + ...headers, + groups: MozillaAccessGroup.NEW_TAB_CURATOR_DEDE, + }; + + const result = await request(app) + .post(url) + .set(unAuthorizedHeaders) + .send({ + query: print(UPDATE_DISMISS_PROSPECT), + variables: { + id: prospect.id, + }, + }); + + expect(result.body.errors).not.to.be.undefined; + expect(result.body.errors?.length).to.equal(1); + expect(result?.body.errors?.[0].message).to.equal( + 'Not authorized for action' + ); + }); + }); +}); diff --git a/servers/prospect-api/src/aws/dynamodb/lib.ts b/servers/prospect-api/src/aws/dynamodb/lib.ts new file mode 100644 index 00000000..5fea1981 --- /dev/null +++ b/servers/prospect-api/src/aws/dynamodb/lib.ts @@ -0,0 +1,164 @@ +import * as Sentry from '@sentry/node'; +import { + DynamoDBDocumentClient, + QueryCommand, + QueryCommandInput, + UpdateCommand, + UpdateCommandInput, +} from '@aws-sdk/lib-dynamodb'; + +import { DynamoItem, GetProspectsFilters, Prospect } from 'prospectapi-common'; + +import { standardizeLanguage } from '../../lib'; +import config from '../../config'; + +/** + * converts a raw item from dynamo db into a Prospect object + * + * @param item raw row from dynamo + * @returns Prospect + */ +export const dynamoItemToProspect = (item: DynamoItem): Prospect => { + return { + id: item?.id, + prospectId: item?.prospectId, + scheduledSurfaceGuid: item?.scheduledSurfaceGuid, + url: item?.url, + prospectType: item?.prospectType, + topic: item?.topic, + saveCount: item?.saveCount, + rank: item?.rank, + createdAt: item?.createdAt, + // note - curated is not exposed in the graphql API + curated: item?.curated, + // the below are optional parser metadata values and may be empty + domain: item?.domain, + excerpt: item?.excerpt, + imageUrl: item?.imageUrl, + language: standardizeLanguage(item?.language), + publisher: item?.publisher, + title: item?.title, + isSyndicated: item?.isSyndicated, + isCollection: item?.isCollection, + authors: item?.authors, + approvedCorpusItem: { url: item?.url }, + rejectedCorpusItem: { url: item?.url }, + }; +}; + +/** + * Retrieves all prospects for the given scheduledSurfaceGuid and (optional) prospectType. + * Additionally, prospects returned can be filtered by publisher (include/exclude). + * + * @param dbClient + * @param filters + * @returns an array of Prospects + */ +export const getProspects = async ( + dbClient: DynamoDBDocumentClient, + filters: GetProspectsFilters +): Promise => { + // base key condition - scheduledSurfaceGuid is required here + let keyConditionExpression = 'scheduledSurfaceGuid = :scheduledSurfaceGuid'; + + // base filter expression: not curated items only + let filterExpression = 'curated = :curated'; + + const expressionAttributeValues = { + ':scheduledSurfaceGuid': filters.scheduledSurfaceGuid, + ':curated': false, // we only return non-curated prospects here + }; + + // augment key condition if prospectType is provided + if (filters.prospectType) { + keyConditionExpression += ' and prospectType = :prospectType'; + expressionAttributeValues[':prospectType'] = filters.prospectType; + } + + /** + * Publisher gets into the mix, if provided. + * + * Unfortunately, filtering is applied post-record retrieval, so + * the results returned will almost always be fewer than a full set (50 records). + * + * Additionally, substring search is case-sensitive: this will have to + * be made clear on the frontend. + */ + if (filters.includePublisher) { + filterExpression += ' AND contains(publisher,:publisher)'; + expressionAttributeValues[':publisher'] = filters.includePublisher; + } + if (filters.excludePublisher) { + filterExpression += ' AND NOT contains(publisher,:publisher)'; + expressionAttributeValues[':publisher'] = filters.excludePublisher; + } + + const input: QueryCommandInput = { + TableName: config.aws.dynamoDb.table, + // this is our Global Secondary Index (GSI) + IndexName: 'scheduledSurfaceGuid-prospectType', + KeyConditionExpression: keyConditionExpression, + ExpressionAttributeValues: expressionAttributeValues, + FilterExpression: filterExpression, + }; + + const res = await dbClient.send(new QueryCommand(input)); + + if (res.Items?.length) { + return res.Items.map((item): Prospect => { + return dynamoItemToProspect(item); + }); + } else { + return []; + } +}; + +/** + * marks a prospect in dynamo as curated so it's filtered out of future client + * requests + * + * @param dbClient + * @param id string id of the prospect in dynamo + */ +export const updateProspectAsCurated = async ( + dbClient: DynamoDBDocumentClient, + id: string +): Promise => { + const input: UpdateCommandInput = { + TableName: config.aws.dynamoDb.table, + Key: { + id: id, + }, + UpdateExpression: 'SET curated = :val', + // if ConditionExpression isn't included, dynamo will just insert a new + // record if the provided id isn't found. sigh. + ConditionExpression: 'attribute_exists(id)', + ExpressionAttributeValues: { + ':val': true, + }, + ReturnValues: 'ALL_NEW', + }; + + try { + const item = await dbClient.send(new UpdateCommand(input)); + + // when performing an update, the item attributes are in `Attributes` + return dynamoItemToProspect(item.Attributes); + } catch (e: unknown) { + // if the provided id cannot be found in dynamo, gracefully fail + // is there an easier way to check the error type below? sheesh... + if ( + e && + typeof e === 'object' && + e['name'] === 'ConditionalCheckFailedException' + ) { + Sentry.captureException( + `failed to mark prospect as curated. id ${id} not found in dynamo.` + ); + + return null; + } else { + throw e; + } + } +}; diff --git a/servers/prospect-api/src/aws/eventBridgeClient.ts b/servers/prospect-api/src/aws/eventBridgeClient.ts new file mode 100644 index 00000000..943ca6b1 --- /dev/null +++ b/servers/prospect-api/src/aws/eventBridgeClient.ts @@ -0,0 +1,6 @@ +import { EventBridgeClient } from '@aws-sdk/client-eventbridge'; +import config from '../config'; + +export const eventBridgeClient = new EventBridgeClient({ + region: config.aws.region, +}); diff --git a/servers/prospect-api/src/config/index.ts b/servers/prospect-api/src/config/index.ts new file mode 100644 index 00000000..1723929a --- /dev/null +++ b/servers/prospect-api/src/config/index.ts @@ -0,0 +1,44 @@ +const snowplowHttpProtocol = + process.env.NODE_ENV === 'production' ? 'https' : 'http'; + +export default { + environment: process.env.NODE_ENV || 'development', + app: { + prospectBatchSize: 50, // # of prospects we return at once to the client + }, + aws: { + localEndpoint: process.env.AWS_ENDPOINT, + region: process.env.AWS_DEFAULT_REGION || 'us-east-1', + dynamoDb: { + table: + process.env.PROSPECT_API_PROSPECTS_TABLE || 'PROAPI-local-Prospects', + }, + eventBus: { + name: + process.env.EVENT_BUS_NAME || 'PocketEventBridge-Dev-Shared-Event-Bus', + eventBridge: { source: 'prospect-events' }, + }, + }, + // environment variables below are set in .aws/src/main.ts + sentry: { + dsn: process.env.SENTRY_DSN || '', + release: process.env.GIT_SHA || '', + environment: process.env.NODE_ENV || 'development', + }, + // The App ID value is used to let the Analytics team know the origin of this event. + eventBridge: { + appId: 'pocket-prospect-api', + }, + snowplow: { + endpoint: process.env.SNOWPLOW_ENDPOINT || 'localhost:9090', + httpProtocol: snowplowHttpProtocol, + bufferSize: 1, + retries: 3, + namespace: 'pocket-backend', + appId: 'pocket-backend-prospect-api', + schemas: { + prospect: 'iglu:com.pocket/prospect/jsonschema/1-0-0', + objectUpdate: 'iglu:com.pocket/object_update/jsonschema/1-0-9', + }, + }, +}; diff --git a/servers/prospect-api/src/context.ts b/servers/prospect-api/src/context.ts new file mode 100644 index 00000000..a7dbbf93 --- /dev/null +++ b/servers/prospect-api/src/context.ts @@ -0,0 +1,45 @@ +import { dbClient } from 'prospectapi-common'; +import { + ScheduledSurfaceGuidToMozillaAccessGroup, + MozillaAccessGroup, + UserAuth, + Context, +} from './types'; + +export const getContext = ({ req }): Context => { + const groups = req.headers.groups as string; + const authGroups = groups ? groups.split(',') : []; + + const userAuth: UserAuth = { + name: req.headers.name as string, + username: req.headers.username as string, + groups: authGroups, + hasReadOnly: authGroups.includes(MozillaAccessGroup.READONLY), + hasCuratorFull: authGroups.includes( + MozillaAccessGroup.SCHEDULED_SURFACE_CURATOR_FULL + ), + // to determine if user has read access + // note that canRead will be only used in queries and not in mutations + canRead: (scheduledSurfaceGuid: string): boolean => + userAuth.hasReadOnly || userAuth.canWrite(scheduledSurfaceGuid), + + // to determine if user can read and write + canWrite: (scheduledSurfaceGuid: string): boolean => { + if (userAuth.hasCuratorFull) { + return true; + } + + const authGroupForScheduledSurface = + ScheduledSurfaceGuidToMozillaAccessGroup[scheduledSurfaceGuid]; + + return userAuth.groups?.includes(authGroupForScheduledSurface) ?? false; + }, + }; + + const context = { + db: dbClient, + userAuth, + }; + + return context; +}; diff --git a/servers/prospect-api/src/events/events.spec.ts b/servers/prospect-api/src/events/events.spec.ts new file mode 100644 index 00000000..e2bb0a92 --- /dev/null +++ b/servers/prospect-api/src/events/events.spec.ts @@ -0,0 +1,196 @@ +import { expect } from 'chai'; +import sinon from 'sinon'; +import * as Sentry from '@sentry/node'; +import { + CorpusLanguage, + Prospect, + ProspectType, + ScheduledSurfaces, + Topics, +} from 'prospectapi-common'; +import { + generateEventBridgePayload, + sendEvent, + sendEventBridgeEvent, +} from './events'; +import { MozillaAccessGroup, UserAuth } from '../types'; +import { EventBridgeEventType, ProspectReviewStatus } from './types'; +import { EventBridgeClient } from '@aws-sdk/client-eventbridge'; +import config from '../config'; +import { serverLogger } from '../express'; + +/** + * These tests are heavily influenced by prior art over at User API + * (see eventBusHandler.spec.ts). + */ +describe('event helpers: ', () => { + const prospect: Prospect = { + // a GUID we generate prior to inserting into dynamo + id: '123-abc', + prospectId: '456-cde', + scheduledSurfaceGuid: ScheduledSurfaces[0].guid, + topic: Topics.ENTERTAINMENT, + prospectType: ProspectType.TIMESPENT, + url: 'https://www.test.com/a-story', + saveCount: 333, + rank: 222, + curated: false, + createdAt: 160000000, + domain: 'test.com', + excerpt: 'Once upon a time...', + imageUrl: 'https://www.test.com/a-story.jpg', + language: CorpusLanguage.EN, + publisher: 'Test.com', + title: 'A very interesting story', + isSyndicated: false, + isCollection: false, + authors: 'Mark Twain, John Bon Jovi', + }; + + const authUser: UserAuth = { + name: 'Test User', + username: 'test-user|ldap-something', + groups: [MozillaAccessGroup.SCHEDULED_SURFACE_CURATOR_FULL], + hasReadOnly: true, + hasCuratorFull: true, + canRead: () => true, + canWrite: () => true, + }; + + const payload = { + prospect: { + ...prospect, + prospectReviewStatus: ProspectReviewStatus.Dismissed, + reviewedBy: authUser.username, + reviewedAt: 1600000, + }, + eventType: EventBridgeEventType.PROSPECT_DISMISS, + object_version: 'new', + }; + + const sandbox = sinon.createSandbox(); + const clientStub = sandbox + .stub(EventBridgeClient.prototype, 'send') + .resolves({ FailedEntryCount: 0 }); + const sentryStub = sandbox.stub(Sentry, 'captureException').resolves(); + const crumbStub = sandbox.stub(Sentry, 'addBreadcrumb').resolves(); + const serverLoggerErrorSpy = sandbox.spy(serverLogger, 'error'); + const now = new Date('2022-01-01 00:00:00'); + let clock; + + beforeAll(() => { + clock = sinon.useFakeTimers({ + now: now, + shouldAdvanceTime: false, + }); + }); + + afterEach(() => sandbox.resetHistory()); + afterAll(() => { + sandbox.restore(); + clock.restore(); + }); + + describe('generateEventBridgePayload function', () => { + it('transforms Prospect object to event payload', () => { + const payload = generateEventBridgePayload(prospect, authUser); + + expect(payload.prospect).to.contain(prospect); + + expect(payload.prospect.prospectReviewStatus).to.equal( + ProspectReviewStatus.Dismissed + ); + expect(payload.prospect.reviewedBy).to.equal(authUser.username); + expect(payload.prospect.reviewedAt).to.be.a('number'); + + expect(payload.eventType).to.equal(EventBridgeEventType.PROSPECT_DISMISS); + expect(payload.object_version).to.equal('new'); + }); + }); + + describe('sendEvent function', () => { + it('should send event to event bus with proper event data', async () => { + await sendEvent(payload); + + // Wait just a tad in case promise needs time to resolve + await setTimeout(() => { + // nothing to see here + }, 100); + expect(sentryStub.callCount).to.equal(0); + expect(serverLoggerErrorSpy.callCount).to.equal(0); + + // Event was sent to Event Bus + expect(clientStub.callCount).to.equal(1); + + // Check that the payload is correct; since it's JSON, we need to decode the data + // otherwise it also does ordering check + const sendCommand = clientStub.getCall(0).args[0].input as any; + expect(sendCommand).to.have.property('Entries'); + expect(sendCommand.Entries[0]).to.contain({ + Source: config.aws.eventBus.eventBridge.source, + EventBusName: config.aws.eventBus.name, + DetailType: EventBridgeEventType.PROSPECT_DISMISS, + }); + + // Compare to initial payload + expect(sendCommand.Entries[0]['Detail']).to.equal( + JSON.stringify(payload) + ); + }); + + it('should log error if any events fail to send', async () => { + clientStub.restore(); + sandbox + .stub(EventBridgeClient.prototype, 'send') + .resolves({ FailedEntryCount: 1 }); + + await sendEvent(payload); + + // Wait just a tad in case promise needs time to resolve + await setTimeout(() => { + // nothing to see here + }, 100); + + expect(sentryStub.callCount).to.equal(1); + expect(sentryStub.getCall(0).firstArg.message).to.contain( + `Failed to send event 'prospect-dismiss' to event bus` + ); + expect(serverLoggerErrorSpy.callCount).to.equal(1); + expect(serverLoggerErrorSpy.getCall(0).firstArg).to.equal( + `sendEvent: Failed to send event to event bus.` + ); + expect(serverLoggerErrorSpy.getCall(0).lastArg.eventType).to.equal( + 'prospect-dismiss' + ); + }); + }); + + describe('sendEventBridgeEvent', () => { + it('should log error if send call throws error', async () => { + clientStub.restore(); + sandbox + .stub(EventBridgeClient.prototype, 'send') + .rejects(new Error('boo!')); + + await sendEventBridgeEvent(prospect, authUser); + + // Wait just a tad in case promise needs time to resolve + setTimeout(() => { + // nothing to see here + }, 100); + expect(sentryStub.callCount).to.equal(1); + expect(sentryStub.getCall(0).firstArg.message).to.contain('boo!'); + expect(crumbStub.callCount).to.equal(1); + expect(crumbStub.getCall(0).firstArg.message).to.contain( + `Failed to send event 'prospect-dismiss' to event bus` + ); + expect(serverLoggerErrorSpy.callCount).to.equal(1); + expect(serverLoggerErrorSpy.getCall(0).firstArg).to.equal( + 'sendEventBridgeEvent: Failed to send event to event bus.' + ); + expect(serverLoggerErrorSpy.getCall(0).lastArg.eventType).to.equal( + 'prospect-dismiss' + ); + }); + }); +}); diff --git a/servers/prospect-api/src/events/events.ts b/servers/prospect-api/src/events/events.ts new file mode 100644 index 00000000..02ad92b0 --- /dev/null +++ b/servers/prospect-api/src/events/events.ts @@ -0,0 +1,119 @@ +import { + PutEventsCommand, + PutEventsCommandOutput, +} from '@aws-sdk/client-eventbridge'; +import * as Sentry from '@sentry/node'; +import { Prospect } from 'prospectapi-common'; + +import config from '../config/'; +import { + EventBridgeEventType, + ProspectDismissEventBusPayload, + ProspectReviewStatus, +} from './types'; +import { UserAuth } from '../types'; +import { eventBridgeClient } from '../aws/eventBridgeClient'; +import { serverLogger } from '../express'; + +/** + * This method sets up the payload to send to Event Bridge for + * the "Dismiss Prospect" event. + * + * @param prospect + * @param authUser + */ +export function generateEventBridgePayload( + prospect: Prospect, + authUser: UserAuth +): ProspectDismissEventBusPayload { + return { + prospect: { + ...prospect, + prospectReviewStatus: ProspectReviewStatus.Dismissed, + reviewedBy: authUser.username, + reviewedAt: Math.round(new Date().getTime() / 1000), + }, + // Note: the one and only event we send from Prospect API is hard-coded here. Is this wise? + eventType: EventBridgeEventType.PROSPECT_DISMISS, + // This is hardcoded, too. + object_version: 'new', + }; +} + +/** + * This is a convenience method called from within a mutation resolver + * to send off event data to Pocket Event Bridge. + * + * @param prospect + * @param authUser + */ +export async function sendEventBridgeEvent(prospect, authUser) { + // Transform mutation data to Event Bridge payload + const payload = generateEventBridgePayload(prospect, authUser); + + // Send to Event Bridge. Yay! + try { + await sendEvent(payload); + } catch (error) { + // In the unlikely event that the payload generator throws an error, + // log to Sentry and Cloudwatch but don't halt program + const failedEventError = new Error( + `Failed to send event '${ + payload.eventType + }' to event bus. Event Body:\n ${JSON.stringify(payload)}` + ); + // Don't halt program, but capture the failure in Sentry and Cloudwatch + Sentry.addBreadcrumb(failedEventError); + Sentry.captureException(error); + serverLogger.error( + 'sendEventBridgeEvent: Failed to send event to event bus.', + { + eventType: payload.eventType, + payload: payload, + error: error, + } + ); + } +} + +/** + * Send event to Event Bus, pulling the event bus and the event source + * from the config. + * Will not throw errors if event fails; instead, log exception to Sentry + * and add to Cloudwatch logs. + * + * Note: copied over from User API + * + * @param eventPayload the payload to send to event bus + */ +export async function sendEvent(eventPayload: any) { + const putEventCommand = new PutEventsCommand({ + Entries: [ + { + EventBusName: config.aws.eventBus.name, + Detail: JSON.stringify(eventPayload), + Source: config.aws.eventBus.eventBridge.source, + DetailType: eventPayload.eventType, + }, + ], + }); + + const output: PutEventsCommandOutput = await eventBridgeClient.send( + putEventCommand + ); + + if (output.FailedEntryCount) { + const failedEventError = new Error( + `Failed to send event '${ + eventPayload.eventType + }' to event bus. Event Body:\n ${JSON.stringify(eventPayload)}` + ); + + // Don't halt program, but capture the failure in Sentry and Cloudwatch + Sentry.captureException(failedEventError); + serverLogger.error('sendEvent: Failed to send event to event bus.', { + eventType: eventPayload.eventType, + payload: eventPayload, + }); + } +} diff --git a/servers/prospect-api/src/events/snowplow-test-helpers.ts b/servers/prospect-api/src/events/snowplow-test-helpers.ts new file mode 100644 index 00000000..c7bd6db0 --- /dev/null +++ b/servers/prospect-api/src/events/snowplow-test-helpers.ts @@ -0,0 +1,62 @@ +import fetch from 'node-fetch'; +import config from '../config'; +import { ProspectReviewStatus, SnowplowProspect } from './types'; + +export const prospect: SnowplowProspect = { + object_version: 'new', + prospect_id: 'c586eff4-f69a-5e5b-8c4d-a4039bb5b497', + url: 'https://www.nytimes.com/2022/11/03/t-magazine/spain-islamic-history.html', + title: 'In Search of a Lost Spain', + excerpt: + 'ON A MORNING of haunting heat in Seville, I sought out the tomb of Ferdinand III. There, in the Gothic cool, older Spaniards came and went, dropping to one knee and crossing themselves before the sepulcher of the Castilian monarch.', + image_url: + 'https://static01.nyt.com/images/2022/11/03/t-magazine/03tmag-spain-slide-9VKO-copy/03tmag-spain-slide-9VKO-facebookJumbo.jpg', + language: 'en', + topic: 'EDUCATION', + is_collection: false, + is_syndicated: false, + authors: ['RICHARD MOSSE', 'AATISH TASEER'], + publisher: 'The New York Times', + domain: 'nytimes.com', + prospect_source: 'COUNTS_LOGISTIC_APPROVAL', + scheduled_surface_id: 'NEW_TAB_EN_US', + created_at: 1668100357, + prospect_review_status: ProspectReviewStatus.Dismissed, + // The Unix timestamp in seconds. + reviewed_at: 1668100358, + // The LDAP string of the curator who reviewed this prospect - for now, only dismissing prospect. + reviewed_by: 'sso-user', +}; + +export async function snowplowRequest( + path: string, + post = false +): Promise { + const response = await fetch( + `${config.snowplow.httpProtocol}://${config.snowplow.endpoint}${path}`, + { + method: post ? 'POST' : 'GET', + } + ); + return await response.json(); +} + +export async function resetSnowplowEvents(): Promise { + await snowplowRequest('/micro/reset', true); +} + +export async function getAllSnowplowEvents(): Promise<{ [key: string]: any }> { + return snowplowRequest('/micro/all'); +} + +export async function getGoodSnowplowEvents(): Promise<{ [key: string]: any }> { + return snowplowRequest('/micro/good'); +} + +export async function getBadSnowplowEvents(): Promise<{ [key: string]: any }> { + return snowplowRequest('/micro/bad'); +} + +export function parseSnowplowData(data: string): { [key: string]: any } { + return JSON.parse(Buffer.from(data, 'base64').toString()); +} diff --git a/servers/prospect-api/src/events/snowplow.integration.ts b/servers/prospect-api/src/events/snowplow.integration.ts new file mode 100644 index 00000000..c9015608 --- /dev/null +++ b/servers/prospect-api/src/events/snowplow.integration.ts @@ -0,0 +1,48 @@ +import { expect } from 'chai'; +import { + getAllSnowplowEvents, + resetSnowplowEvents, + prospect, +} from './snowplow-test-helpers'; +import { getEmitter, getTracker, queueSnowplowEvent } from './snowplow'; + +describe('snowplow', () => { + beforeEach(async () => { + await resetSnowplowEvents(); + }); + + it('should accept an event with a prospect', async () => { + const emitter = getEmitter(); + const tracker = getTracker(emitter); + + queueSnowplowEvent(tracker, 'prospect_reviewed', prospect); + + // wait a sec * 3 + await new Promise((resolve) => setTimeout(resolve, 3000)); + + const allEvents = await getAllSnowplowEvents(); + + // context for the expect's below: + // + // in ./src/aws/dynamodb/lib.integration.ts, three snowplow events are + // triggered by hitting the dismiss resolver. this is fine. however, the + // problem is that for some reason the snowplow events are not being + // cleared, even with the explicit call in beforeEach above. this results + // in CI reporting 4 good events instead of 1 - but only most of the time! + // sometimes, it will report a single good event. my guess here is that + // jest is not actually running tests serially, even with `--runInBand` + // specifically set. this is a problem for another day. + // + // so, for now, skip checking the total number of events (it will either be + // 1 or 4). + //expect(allEvents.total).to.equal(1); + + // this isn't a great test, but we can at least verify a good event exists + // (could it be a good event from a different test file? yep!) + expect(allEvents.good).to.be.greaterThan(0); + + // we can check that we have zero bad events - this is the one real expect + // in this test. + expect(allEvents.bad).to.equal(0); + }); +}); diff --git a/servers/prospect-api/src/events/snowplow.ts b/servers/prospect-api/src/events/snowplow.ts new file mode 100644 index 00000000..de6315c7 --- /dev/null +++ b/servers/prospect-api/src/events/snowplow.ts @@ -0,0 +1,136 @@ +import * as Sentry from '@sentry/node'; +import { + gotEmitter, + HttpMethod, + HttpProtocol, + tracker as snowPlowTracker, + buildSelfDescribingEvent, + Emitter, + Tracker, + SelfDescribingEvent, + SelfDescribingJson, +} from '@snowplow/node-tracker'; +import { Response, RequestError } from 'got'; + +import config from '../config'; +import { SnowplowProspect } from './types'; +import { serverLogger } from '../express'; + +let emitter: Emitter; +let tracker: Tracker; + +/** + * lazy instantiation of a snowplow emitter + * + * @returns Emitter + */ +export function getEmitter(): Emitter { + if (!emitter) { + emitter = gotEmitter( + config.snowplow.endpoint, + config.snowplow.httpProtocol as HttpProtocol, + undefined, + HttpMethod.POST, + config.snowplow.bufferSize, + config.snowplow.retries, + undefined, + // this is the callback function invoked after snowplow flushes their + // internal cache. + (error?: RequestError, response?: Response) => { + if (error) { + serverLogger.error('emitter error', { error }); + Sentry.addBreadcrumb({ message: 'Emitter Data', data: error }); + Sentry.captureMessage(`Emitter Error`); + } + } + ); + } + + return emitter; +} + +/** + * lazy instantiation of a snowplow tracker + * @param emitter Emitter - a snowplow emitter + * @returns Tracker + */ +export const getTracker = (emitter: Emitter): Tracker => { + if (!tracker) { + tracker = snowPlowTracker( + emitter, + config.snowplow.namespace, + config.snowplow.appId, + true + ); + } + + return tracker; +}; + +/** + * creates a snowplow event object + * + * @param eventName string - the name of the event from dynamo + * @returns SelfDescribingEvent + */ +export const generateEvent = (eventName: string): SelfDescribingEvent => { + return { + event: { + schema: config.snowplow.schemas.objectUpdate, + data: { + trigger: eventName.toLowerCase(), + object: 'prospect', + }, + }, + }; +}; + +/** + * generates JSON for an entity (prospect) being sent to snowplow + * + * @param prospect PocketAnalyticsArticle + * @returns SelfDescribingJson + */ +export const generateContext = ( + prospect: SnowplowProspect +): SelfDescribingJson => { + return { + schema: config.snowplow.schemas.prospect, + data: prospect, + }; +}; + +/** + * main entry point to snowplow. queues up an event to send. + * + * (elsewhere, we tell snowplow to send all queued events.) + * + * @param tracker TrackerInterface + * @param eventName string + * @param prospect SnowplowProspect + * @returns void + */ +export const queueSnowplowEvent = ( + tracker: Tracker, + eventName: string, + prospect: SnowplowProspect +): void => { + const event = generateEvent(eventName); + const contexts: SelfDescribingJson[] = [generateContext(prospect)]; + + try { + // reminder - this method is not async and does not directly initiate + // any http request. it sends the event to a queue internal to the + // snowplow module, which has its own logic on when to flush the queue. + tracker.track(buildSelfDescribingEvent(event), contexts); + } catch (ex) { + // send the error to sentry and log it for investigation purposes + const message = `Failed to send event to snowplow.\n event: ${event}\n contexts: ${contexts}`; + serverLogger.error('Failed to send event to snowplow', { + event: event, + contexts: contexts, + }); + Sentry.addBreadcrumb({ message }); + Sentry.captureException(ex); + } +}; diff --git a/servers/prospect-api/src/events/types.ts b/servers/prospect-api/src/events/types.ts new file mode 100644 index 00000000..7f8352cc --- /dev/null +++ b/servers/prospect-api/src/events/types.ts @@ -0,0 +1,76 @@ +export enum ProspectReviewStatus { + Created = 'created', + Recommendation = 'recommendation', + Corpus = 'corpus', + Rejected = 'rejected', + Dismissed = 'dismissed', // the only one being implemented +} + +export type EventBridgeProspect = { + // a GUID we generate prior to inserting into dynamo + id: string; + // the prospect ID supplied by ML + prospectId: string; + scheduledSurfaceGuid: string; + topic?: string; + prospectType: string; + url: string; + saveCount: number; + rank: number; + curated?: boolean; + // unix timestamp + createdAt?: number; + domain?: string; + excerpt?: string; + imageUrl?: string; + language?: string; + publisher?: string; + title?: string; + isSyndicated?: boolean; + isCollection?: boolean; + // authors will be a comma separated string + authors?: string; + approvedCorpusItem?: { url: string }; + rejectedCorpusItem?: { url: string }; + prospectReviewStatus: ProspectReviewStatus; + // The LDAP string of the curator who reviewed this prospect - for now, only dismissing prospect. + reviewedBy?: string; + // The Unix timestamp in seconds. + reviewedAt?: number; +}; + +export enum EventBridgeEventType { + PROSPECT_DISMISS = 'prospect-dismiss', +} + +export type ProspectDismissEventBusPayload = { + eventType: EventBridgeEventType; + object_version: string; + prospect: EventBridgeProspect; +}; + +// referenced from snowplow schema directly +export type SnowplowProspect = { + object_version: 'new' | 'old'; + // the prospect ID supplied by ML + prospect_id: string; + url: string; + title?: string; + excerpt?: string; + image_url?: string; + language?: string; + topic?: string; + is_collection?: boolean; + is_syndicated?: boolean; + authors?: string[]; + publisher?: string; + domain?: string; + prospect_source: string; + scheduled_surface_id: string; + created_at: number; + prospect_review_status: ProspectReviewStatus; + // The Unix timestamp in seconds. + reviewed_at?: number; + // The LDAP string of the curator who reviewed this prospect - for now, only dismissing prospect. + reviewed_by?: string; +}; diff --git a/servers/prospect-api/src/express.ts b/servers/prospect-api/src/express.ts new file mode 100644 index 00000000..9228b3a2 --- /dev/null +++ b/servers/prospect-api/src/express.ts @@ -0,0 +1,54 @@ +import * as Sentry from '@sentry/node'; +import express from 'express'; +import http from 'http'; +import cors from 'cors'; +import { ApolloServer } from '@apollo/server'; +import { expressMiddleware } from '@apollo/server/express4'; +//See https://github.com/jaydenseric/graphql-upload/issues/305#issuecomment-1135285811 on why we do this +import config from './config'; +import { startApolloServer } from './server'; +import { getContext } from './context'; +import { AdminAPIUserContext } from './types'; +import { setLogger, setMorgan } from '@pocket-tools/ts-logger'; + +export const serverLogger: any = setLogger(); + +/** + * Initialize an express server + * + * @param port number + */ +export async function startServer(port: number): Promise<{ + app: Express.Application; + apolloServer: ApolloServer; + url: string; +}> { + Sentry.init({ + ...config.sentry, + debug: config.sentry.environment === 'development', + }); + + // initialize express with exposed httpServer so that it may be + // provided to drain plugin for graceful shutdown. + const app = express(); + const httpServer = http.createServer(app); + // JSON parser to enable POST body with JSON + app.use(express.json(), setMorgan(serverLogger)); + // expose a health check url + app.get('/.well-known/apollo/server-health', (req, res) => { + res.status(200).send('ok'); + }); + + const apolloServer = await startApolloServer(httpServer); + const url = '/'; + + app.use( + cors(), + expressMiddleware(apolloServer, { + context: async ({ req }) => getContext({ req }), + }) + ); + + await new Promise((resolve) => httpServer.listen({ port }, resolve)); + return { app, apolloServer, url }; +} diff --git a/servers/prospect-api/src/lib.spec.ts b/servers/prospect-api/src/lib.spec.ts new file mode 100644 index 00000000..14ec73f4 --- /dev/null +++ b/servers/prospect-api/src/lib.spec.ts @@ -0,0 +1,380 @@ +import { faker } from '@faker-js/faker'; +import { expect } from 'chai'; + +import config from './config'; +import { + getScheduledSurfaceByGuid, + getSortedRankedProspects, + getRandomizedSortedRankedProspects, + isValidProspectType, + standardizeLanguage, + deDuplicateProspectUrls, +} from './lib'; +import { Prospect, ProspectType, Topics } from 'prospectapi-common'; + +// turn the enum into an array so we can grab a random one easy peasy +const topicsArray = Object.keys(Topics).map((key) => Topics[key]); + +// TODO: refactor into a seeder-type helper for all tests? +const makeProspects = ( + count: number, + options?: Partial +): Prospect[] => { + const prospects: Prospect[] = []; + + for (let i = 0; i < count; i++) { + prospects.push({ + id: faker.datatype.uuid(), + prospectId: faker.datatype.uuid(), + scheduledSurfaceGuid: 'NEW_TAB_EN_US', + prospectType: ProspectType.COUNTS, + topic: faker.random.arrayElement(topicsArray), + url: faker.internet.url(), + createdAt: Math.floor(faker.date.recent().valueOf() / 1000), + saveCount: faker.datatype.number(), + rank: faker.datatype.number(), + ...options, + }); + } + + return prospects; +}; + +describe('lib', () => { + describe('isValidScheduledSurfaceGuid', () => { + it('should return true for a valid scheduled surface GUID', () => { + expect(getScheduledSurfaceByGuid('NEW_TAB_EN_US')).not.to.be.undefined; + }); + + it('should return false for an invalid scheduled surface GUID', () => { + expect(getScheduledSurfaceByGuid('NEW_TAB_CY_GB')).to.be.undefined; // Welsh + }); + }); + + describe('isValidProspectType', () => { + it('should return true for a valid prospect type', () => { + expect(isValidProspectType('NEW_TAB_EN_US', ProspectType.SYNDICATED_NEW)) + .to.be.true; + + expect(isValidProspectType('NEW_TAB_DE_DE', ProspectType.COUNTS)).to.be + .true; + }); + + it('should return false for an invalid prospect type', () => { + expect(isValidProspectType('NEW_TAB_DE_DE', ProspectType.SYNDICATED_NEW)) + .to.be.false; + }); + }); + + describe('standardizeLanguage', () => { + it('should standardize valid corpus languages', () => { + expect(standardizeLanguage('en')).to.equal('EN'); + expect(standardizeLanguage('EN')).to.equal('EN'); + + expect(standardizeLanguage('de')).to.equal('DE'); + expect(standardizeLanguage('DE')).to.equal('DE'); + + expect(standardizeLanguage('es')).to.equal('ES'); + expect(standardizeLanguage('ES')).to.equal('ES'); + + expect(standardizeLanguage('fr')).to.equal('FR'); + expect(standardizeLanguage('FR')).to.equal('FR'); + + expect(standardizeLanguage('it')).to.equal('IT'); + expect(standardizeLanguage('IT')).to.equal('IT'); + }); + + it('should return undefined if a non-corpus language is passed', () => { + expect(standardizeLanguage('hi')).to.be.undefined; + expect(standardizeLanguage('ga')).to.be.undefined; + expect(standardizeLanguage('xx')).to.be.undefined; + }); + + it('should return undefined if null is passed', () => { + expect(standardizeLanguage()).to.be.undefined; + }); + }); + + describe('getSortedRankedProspects', () => { + it('should sort by descending rank', () => { + const prospects: Prospect[] = []; + + prospects.push({ + id: faker.datatype.uuid(), + prospectId: faker.datatype.uuid(), + scheduledSurfaceGuid: 'NEW_TAB_EN_US', + prospectType: ProspectType.COUNTS, + topic: faker.random.arrayElement(topicsArray), + url: faker.internet.url(), + createdAt: Math.floor(faker.date.recent().valueOf() / 1000), + saveCount: faker.datatype.number(), + rank: 10, + }); + + prospects.push({ + id: faker.datatype.uuid(), + prospectId: faker.datatype.uuid(), + scheduledSurfaceGuid: 'NEW_TAB_EN_US', + prospectType: ProspectType.COUNTS, + topic: faker.random.arrayElement(topicsArray), + url: faker.internet.url(), + createdAt: Math.floor(faker.date.recent().valueOf() / 1000), + saveCount: faker.datatype.number(), + rank: 20, + }); + + prospects.push({ + id: faker.datatype.uuid(), + prospectId: faker.datatype.uuid(), + scheduledSurfaceGuid: 'NEW_TAB_EN_US', + prospectType: ProspectType.COUNTS, + topic: faker.random.arrayElement(topicsArray), + url: faker.internet.url(), + createdAt: Math.floor(faker.date.recent().valueOf() / 1000), + saveCount: faker.datatype.number(), + rank: 20, + }); + + prospects.push({ + id: faker.datatype.uuid(), + prospectId: faker.datatype.uuid(), + scheduledSurfaceGuid: 'NEW_TAB_EN_US', + prospectType: ProspectType.SYNDICATED_NEW, + topic: faker.random.arrayElement(topicsArray), + url: faker.internet.url(), + createdAt: Math.floor(faker.date.recent().valueOf() / 1000), + saveCount: faker.datatype.number(), + rank: 10, + }); + + prospects.push({ + id: faker.datatype.uuid(), + prospectId: faker.datatype.uuid(), + scheduledSurfaceGuid: 'NEW_TAB_EN_US', + prospectType: ProspectType.SYNDICATED_NEW, + topic: faker.random.arrayElement(topicsArray), + url: faker.internet.url(), + createdAt: Math.floor(faker.date.recent().valueOf() / 1000), + saveCount: faker.datatype.number(), + rank: 20, + }); + + const result = getSortedRankedProspects(prospects); + + console.log(result); + + // we have two prospect types + expect(Object.keys(result).length).to.equal(2); + + expect(result.COUNTS.length).to.equal(3); + expect(result.COUNTS[0].rank).to.equal(20); + expect(result.COUNTS[1].rank).to.equal(20); + expect(result.COUNTS[2].rank).to.equal(10); + + expect(result.SYNDICATED_NEW.length).to.equal(2); + expect(result.SYNDICATED_NEW[0].rank).to.equal(20); + expect(result.SYNDICATED_NEW[1].rank).to.equal(10); + }); + }); + + describe('getRandomizedSortedRankedProspects', () => { + it('should return a randomized list of prospects where each prospect has the next top rank for its prospect type', () => { + const prospects: Prospect[] = []; + + // push two prospects each of 3 different prospect types and different ranks + prospects.push({ + id: faker.datatype.uuid(), + prospectId: faker.datatype.uuid(), + scheduledSurfaceGuid: 'NEW_TAB_EN_US', + prospectType: ProspectType.COUNTS, + topic: faker.random.arrayElement(topicsArray), + url: faker.internet.url(), + createdAt: Math.floor(faker.date.recent().valueOf() / 1000), + saveCount: faker.datatype.number(), + rank: 10, + }); + + prospects.push({ + id: faker.datatype.uuid(), + prospectId: faker.datatype.uuid(), + scheduledSurfaceGuid: 'NEW_TAB_EN_US', + prospectType: ProspectType.COUNTS, + topic: faker.random.arrayElement(topicsArray), + url: faker.internet.url(), + createdAt: Math.floor(faker.date.recent().valueOf() / 1000), + saveCount: faker.datatype.number(), + rank: 20, + }); + + prospects.push({ + id: faker.datatype.uuid(), + prospectId: faker.datatype.uuid(), + scheduledSurfaceGuid: 'NEW_TAB_EN_US', + prospectType: ProspectType.TIMESPENT, + topic: faker.random.arrayElement(topicsArray), + url: faker.internet.url(), + createdAt: Math.floor(faker.date.recent().valueOf() / 1000), + saveCount: faker.datatype.number(), + rank: 30, + }); + + prospects.push({ + id: faker.datatype.uuid(), + prospectId: faker.datatype.uuid(), + scheduledSurfaceGuid: 'NEW_TAB_EN_US', + prospectType: ProspectType.TIMESPENT, + topic: faker.random.arrayElement(topicsArray), + url: faker.internet.url(), + createdAt: Math.floor(faker.date.recent().valueOf() / 1000), + saveCount: faker.datatype.number(), + rank: 60, + }); + + prospects.push({ + id: faker.datatype.uuid(), + prospectId: faker.datatype.uuid(), + scheduledSurfaceGuid: 'NEW_TAB_EN_US', + prospectType: ProspectType.SYNDICATED_NEW, + topic: faker.random.arrayElement(topicsArray), + url: faker.internet.url(), + createdAt: Math.floor(faker.date.recent().valueOf() / 1000), + saveCount: faker.datatype.number(), + rank: 40, + }); + + prospects.push({ + id: faker.datatype.uuid(), + prospectId: faker.datatype.uuid(), + scheduledSurfaceGuid: 'NEW_TAB_EN_US', + prospectType: ProspectType.SYNDICATED_NEW, + topic: faker.random.arrayElement(topicsArray), + url: faker.internet.url(), + createdAt: Math.floor(faker.date.recent().valueOf() / 1000), + saveCount: faker.datatype.number(), + rank: 50, + }); + + const sortedRankedProspects = getSortedRankedProspects(prospects); + const result = getRandomizedSortedRankedProspects(sortedRankedProspects); + + // pull out the COUNTS prospect type prospects since the order of the results is randomized + const countsProspects = result.filter((item) => { + return item.prospectType === ProspectType.COUNTS; + }); + // pull out the SYNDICATED_NEW prospect type prospects + const syndicatedProspects = result.filter((item) => { + return item.prospectType === ProspectType.SYNDICATED_NEW; + }); + + // pull out the TIMESPENT prospect type prospects + const organicTimespentProspects = result.filter((item) => { + return item.prospectType === ProspectType.TIMESPENT; + }); + + // number of results should be the same as the original list of prospects + expect(result.length).to.equal(6); + + // the two COUNTS prospects should be in ascending order based on their rank + expect(countsProspects[0].rank).to.be.below(countsProspects[1].rank); + + // the two SYNDICATED_NEW prospects should be in ascending order based on their rank + expect(syndicatedProspects[0].rank).to.be.below( + syndicatedProspects[1].rank + ); + + // the two TIMESPENT prospects should be in ascending order based on their rank + expect(organicTimespentProspects[0].rank).to.be.below( + organicTimespentProspects[1].rank + ); + }); + + it('should return an empty array when no prospects are provided', () => { + const prospects: Prospect[] = []; + + const sortedRankedProspects = getSortedRankedProspects(prospects); + const result = getRandomizedSortedRankedProspects(sortedRankedProspects); + + expect(result.length).to.equal(0); + }); + + it('should return a list of prospects of default batch size if more are provided', () => { + const prospects: Prospect[] = []; + + const baseProspect = { + id: faker.datatype.uuid(), + prospectId: faker.datatype.uuid(), + scheduledSurfaceGuid: 'NEW_TAB_EN_US', + prospectType: ProspectType.COUNTS, + topic: faker.random.arrayElement(topicsArray), + url: faker.internet.url(), + createdAt: Math.floor(faker.date.recent().valueOf() / 1000), + saveCount: faker.datatype.number(), + rank: 10, + }; + + // push 55 prospects which is more than our default batch size of 50 + for (let i = 0; i < 55; i++) { + prospects.push({ + ...baseProspect, + prospectType: + i % 2 === 0 ? ProspectType.COUNTS : ProspectType.SYNDICATED_NEW, + rank: Math.floor(Math.random() * 100), + }); + } + + const sortedRankedProspects = getSortedRankedProspects(prospects); + const result = getRandomizedSortedRankedProspects(sortedRankedProspects); + + // we should get a list equal to default batch size + expect(result.length).to.equal(config.app.prospectBatchSize); + }); + }); + + //TODO: fix this test @Herraj + describe.skip('findAndLogTrueDuplicateProspects', () => { + it('should not call sentry if no true duplicates are found', () => { + // chai.use(spies); + // const prospects: Prospect[] = makeProspects(3); + //const sentrySpy = chai.spy.on(Sentry, 'captureMessage'); + // findAndLogTrueDuplicateProspects(prospects); + //expect(sentrySpy).to.not.have.been.called(); + }); + }); + + describe('deDuplicateProspectUrls', () => { + it('should return the same length array as the original when no duplicates are found', () => { + const prospects: Prospect[] = makeProspects(3); + + const deDupedProspects = deDuplicateProspectUrls(prospects); + + // since no duplicates exist, resultant de-duped array should be same length as original + expect(deDupedProspects.length).to.equal(prospects.length); + }); + + it('should return the de-duplicated prospect list', () => { + const prospects: Prospect[] = makeProspects(3); + + const testProspect = { + id: 'dupe', + prospectId: faker.datatype.uuid(), + scheduledSurfaceGuid: 'NEW_TAB_EN_US', + prospectType: ProspectType.COUNTS, + topic: faker.random.arrayElement(topicsArray), + url: faker.internet.url(), + createdAt: Math.floor(faker.date.recent().valueOf() / 1000), + saveCount: faker.datatype.number(), + rank: 10, + }; + + // adding three of the same prospects with the same url, total array length is 6 + prospects.push(testProspect); + prospects.push(testProspect); + prospects.push(testProspect); + + const deDupedProspects = deDuplicateProspectUrls(prospects); + + // length of returned array should be 4 after removing 2 duplicates + expect(deDupedProspects.length).to.equal(4); + }); + }); +}); diff --git a/servers/prospect-api/src/lib.ts b/servers/prospect-api/src/lib.ts new file mode 100644 index 00000000..4506faf4 --- /dev/null +++ b/servers/prospect-api/src/lib.ts @@ -0,0 +1,280 @@ +import * as Sentry from '@sentry/node'; +import { cloneDeep } from 'lodash'; + +import { + Prospect, + ScheduledSurface, + ScheduledSurfaces, +} from 'prospectapi-common'; + +import config from './config'; +import { CorpusLanguage, SortedRankedProspects } from './types'; +import { ProspectReviewStatus, SnowplowProspect } from './events/types'; + +/** + * checks the given new tab GUID to make sure it's valid + * + * @param guid string scheduled surface GUID provided by clients when querying + * @returns ScheduledSurface if valid GUID, undefined if not + */ +export const getScheduledSurfaceByGuid = ( + guid: string +): ScheduledSurface | undefined => { + return ScheduledSurfaces.find( + (surface: ScheduledSurface) => surface.guid === guid + ); +}; + +/** + * checks the given prospect type is valid based on the new tab GUID + * (each new tab has a subset of prospect types that are valid) + * + * @param scheduledSurfaceGuid string new tab GUID provided by clients when querying + * @param prospectType string prospect type provided by clients + * @returns boolean + */ +export const isValidProspectType = ( + scheduledSurfaceGuid: string, + prospectType: string +): boolean => { + // get the new tab (already validated using the above function) + const scheduledSurface: ScheduledSurface = ScheduledSurfaces.filter( + (scheduledSurface) => { + return scheduledSurface.guid === scheduledSurfaceGuid; + } + )[0]; + + let isValid = false; + + // make sure the prospect type is associated with the new tab + scheduledSurface.prospectTypes.forEach((pt) => { + if (pt === prospectType) { + isValid = true; + } + }); + + return isValid; +}; + +/** + * + * @param prospects an array of prospects from dynamodb + * @returns an object with keys matching available prospect types, each key + * containing an array of prospects matching that prospect type sorted by + * rank, ascending + */ +export const getSortedRankedProspects = ( + prospects: Prospect[] +): SortedRankedProspects => { + // get all unique prospectTypes from `prospects` - this will be our random choice + const availableProspectTypes: string[] = []; + + prospects.forEach((prospect) => { + if (!availableProspectTypes.includes(prospect.prospectType)) { + availableProspectTypes.push(prospect.prospectType); + } + }); + + // break up `prospects` into smaller arrays by prospectType + const sortedProspects: SortedRankedProspects = {}; + + // add an object key for each prospect type pointing to an empty prospect + availableProspectTypes.forEach((val) => { + sortedProspects[val] = []; + }); + + // put prospects into prospect type arrays + prospects.forEach((p) => { + sortedProspects[p.prospectType].push(p); + }); + + // for each smaller array, sort by rank (descending) because we need to pop from the array + availableProspectTypes.forEach((val) => { + sortedProspects[val].sort((a: Prospect, b: Prospect) => { + return b.rank - a.rank; + }); + }); + + return sortedProspects; +}; + +/** + * + * @param sortedProspects + * @returns An array of max 20 randomized prospects where each prospect is the next highest + * ranked for its prospect type + */ +export const getRandomizedSortedRankedProspects = ( + sortedProspects: SortedRankedProspects +): Prospect[] => { + // deep cloning the method parameter to avoid mutating it + const sortedProspectsClone = cloneDeep(sortedProspects); + + // pull out the prospect types from the sorted prospects + const prospectTypes = Object.keys(sortedProspectsClone); + // get the total number of prospects for all prospect types + const totalSortedProspects = + Object.values(sortedProspectsClone).flat().length; + + let randomProspectType: string; + let highestRankedProspectForProspectType: Prospect | undefined; + let randomIdx: number; + let prospectsProcessed = 0; + + // if the total number of prospects is less than the config prospectBatchSize, use it as the batch size + const batchSize = + totalSortedProspects < config.app.prospectBatchSize + ? totalSortedProspects + : config.app.prospectBatchSize; + + const result: Prospect[] = []; + + // keep going until we've processed all prospects + while (prospectsProcessed != batchSize) { + randomIdx = Math.floor(Math.random() * prospectTypes.length); + + // get a random prospect type + randomProspectType = prospectTypes[randomIdx]; + + // get the highest(lower number = higher rank) ranked prospect for a random prospect type + // since each prospect type has its prospects sorted in descending order, pop() gets us the last element which is the highest ranked + highestRankedProspectForProspectType = + sortedProspectsClone[randomProspectType].pop(); + + // if the chosen prospect type is now empty, remove it from our available choices + if (sortedProspects[randomProspectType].length === 0) { + // remove the prospect type which has no prospects left + prospectTypes.splice(randomIdx, 1); + } + + // push to our result array + if (highestRankedProspectForProspectType) { + result.push(highestRankedProspectForProspectType); + prospectsProcessed++; + } + } + + // Log a message to sentry if we get prospects fewer than default batch size + batchSize < config.app.prospectBatchSize && + Sentry.captureMessage( + `Found prospects fewer than default batch size: ${batchSize}` + ); + + return result; +}; + +/** + * ensure we send either a valid CorpusLanguage enum or undefined to clients + * + * @param language string value returned from the parser + * @returns either a CorpusLanguage enum or undefined + */ +export const standardizeLanguage = ( + language?: string +): CorpusLanguage | undefined => { + return language ? CorpusLanguage[language.toUpperCase()] : undefined; +}; + +/** + * + * @param prospects an array of prospects + * @returns prospects sorted by their rank in ascending order + */ +export const getProspectsSortedByAscendingRank = ( + prospects: Prospect[] +): Prospect[] => { + return prospects.sort((a: Prospect, b: Prospect) => { + return a.rank - b.rank; + }); +}; + +/** + * helper to find and log to Sentry the true duplicate prospects in an array of prospects + * a true duplicate means having the same id, prospectType and scheduledSurfaceGuid combo + * + * @param prospects + */ +export const findAndLogTrueDuplicateProspects = ( + prospects: Prospect[] +): void => { + const trueDuplicateProspects: Prospect[] = []; + const duplicateProspectProps = new Set(); + + for (const prospect of prospects) { + const dupeProps = JSON.stringify({ + id: prospect.id, + prospectType: prospect.prospectType, + scheduledSurfaceGuid: prospect.scheduledSurfaceGuid, + }); + + if (duplicateProspectProps.has(dupeProps)) { + trueDuplicateProspects.push(prospect); + } + + duplicateProspectProps.add(dupeProps); + } + + if (trueDuplicateProspects.length > 0) { + Sentry.captureMessage( + `True duplicate prospects found: ${[...trueDuplicateProspects]}` + ); + } +}; + +/** + * helper to de-duplicate prospects based on url. Logs to Sentry with list of duplicate urls + * + * @param prospects + * @returns deDupedProspects + */ +export const deDuplicateProspectUrls = (prospects: Prospect[]): Prospect[] => { + const duplicateUrls = new Set(); + const prospectUrlSet = new Set(); + const deDupedProspects: Prospect[] = []; + + for (const prospect of prospects) { + if (!prospectUrlSet.has(prospect.url)) { + deDupedProspects.push(prospect); + } else { + duplicateUrls.add(prospect.url); + } + + prospectUrlSet.add(prospect.url); + } + + return deDupedProspects; +}; + +/** + * converts a Prospect into its snowplow equivalent + * + * @param prospect a Prospect object + * @returns a SnowplowProspect object + */ +export const prospectToSnowplowProspect = ( + prospect: Prospect, + authUserName: string +): SnowplowProspect => { + return { + object_version: 'new', + prospect_id: prospect.prospectId, + url: prospect.url, + title: prospect.title, + excerpt: prospect.excerpt, + image_url: prospect.imageUrl, + language: prospect.language, + topic: prospect.topic, + is_collection: prospect.isCollection, + is_syndicated: prospect.isSyndicated, + authors: prospect.authors?.split(','), + publisher: prospect.publisher, + domain: prospect.domain, + prospect_source: prospect.prospectType, + scheduled_surface_id: prospect.scheduledSurfaceGuid, + // not sure how a prospect could be missing a `createdAt` value... + created_at: prospect.createdAt || Date.now(), + prospect_review_status: ProspectReviewStatus.Dismissed, + reviewed_at: Date.now(), + reviewed_by: authUserName, + }; +}; diff --git a/servers/prospect-api/src/main.ts b/servers/prospect-api/src/main.ts new file mode 100644 index 00000000..037017b5 --- /dev/null +++ b/servers/prospect-api/src/main.ts @@ -0,0 +1,6 @@ +import { serverLogger, startServer } from './express'; + +(async () => { + const { url } = await startServer(4026); + serverLogger.info(`🚀 Server ready at http://localhost:4026${url}`); +})(); diff --git a/servers/prospect-api/src/resolvers.ts b/servers/prospect-api/src/resolvers.ts new file mode 100644 index 00000000..79fbaeb9 --- /dev/null +++ b/servers/prospect-api/src/resolvers.ts @@ -0,0 +1,165 @@ +import { + AuthenticationError, + UserInputError, +} from '@pocket-tools/apollo-utils'; + +import { + deriveUrlMetadata, + Prospect, + UrlMetadata, + getProspectById, +} from 'prospectapi-common'; + +import { + getProspects, + updateProspectAsCurated, + dynamoItemToProspect, +} from './aws/dynamodb/lib'; +import config from './config'; + +import { + getScheduledSurfaceByGuid, + isValidProspectType, + getRandomizedSortedRankedProspects, + getProspectsSortedByAscendingRank, + getSortedRankedProspects, + findAndLogTrueDuplicateProspects, + deDuplicateProspectUrls, + prospectToSnowplowProspect, +} from './lib'; + +import { GetProspectsFilters, Context } from './types'; +//import { sendEventBridgeEvent } from './events/events'; +import { getEmitter, getTracker, queueSnowplowEvent } from './events/snowplow'; + +/** + * Return an object conforming to the Item graphql definition. + * + * @param parent // a ProspectItem + */ +export const ItemResolver = (parent) => { + return { + givenUrl: parent.url, + }; +}; + +export const resolvers = { + Prospect: { + item: ItemResolver, + }, + Query: { + getProspects: async ( + parent, + { filters }: GetProspectsFilters, + { db, userAuth }: Context + ): Promise => { + const scheduledSurface = getScheduledSurfaceByGuid( + filters.scheduledSurfaceGuid + ); + + // validate filters + if (scheduledSurface === undefined) { + throw new UserInputError( + `${filters.scheduledSurfaceGuid} isn't a valid scheduled surface guid!` + ); + } + + if (filters.prospectType) { + if ( + !isValidProspectType( + filters.scheduledSurfaceGuid, + filters.prospectType + ) + ) { + throw new UserInputError( + `${filters.prospectType} is not a valid prospect type for scheduled surface ${scheduledSurface.name}` + ); + } + } + // check if user has read access for this query + if (!userAuth.canRead(filters.scheduledSurfaceGuid)) { + throw new AuthenticationError('Not authorized for action'); + } + + // get prospects + let prospects: Prospect[] = await getProspects(db, filters); + + // de-duplicate prospects based on duplicate urls + prospects = deDuplicateProspectUrls(prospects); + + if (filters.prospectType) { + // sort prospects by ascending rank and return the default batch size of them + prospects = getProspectsSortedByAscendingRank(prospects).slice( + 0, + config.app.prospectBatchSize + ); + } else { + // randomize by prospect type and sort by descending rank + const sortedRankedProspects = getSortedRankedProspects(prospects); + prospects = getRandomizedSortedRankedProspects(sortedRankedProspects); + } + + // check if the prospects have any true duplicates and log to Sentry + findAndLogTrueDuplicateProspects(prospects); + + return prospects; + }, + getUrlMetadata: async (parent, { url }, ctx): Promise => { + let itemUrl = ''; + try { + // validate url by throwing if url format is incorrect + itemUrl = new URL(url).toString(); + } catch (error) { + throw new UserInputError(`${url} is not a valid url `); + } + return await deriveUrlMetadata(itemUrl); + }, + }, + Mutation: { + updateProspectAsCurated: async ( + parent, + { id }, + { db, userAuth }: Context + ): Promise => { + // fetch prospect from db first + const prospect = dynamoItemToProspect(await getProspectById(db, id)); + + // check if user has write access for this mutation + if (!userAuth.canWrite(prospect.scheduledSurfaceGuid)) { + throw new AuthenticationError('Not authorized for action'); + } + + return updateProspectAsCurated(db, id); + }, + dismissProspect: async ( + parent, + { id }, + { db, userAuth }: Context + ): Promise => { + // fetch prospect from db first + const prospect = dynamoItemToProspect(await getProspectById(db, id)); + + // check if user has write access for this mutation + if (!userAuth.canWrite(prospect.scheduledSurfaceGuid)) { + throw new AuthenticationError('Not authorized for action'); + } + + // 2022-11-10: event bridge on pause while system stability is improved. + // will go back to this code/send when event bridge is ready. + // Send the 'Dismiss' event to Pocket event bridge + // await sendEventBridgeEvent(prospect, userAuth); + + // in the mean time, send the dismiss event directly to snowplow + // initialize snowplow tracker + const snowplowEmitter = getEmitter(); + const snowplowTracker = getTracker(snowplowEmitter); + queueSnowplowEvent( + snowplowTracker, + 'prospect_reviewed', + prospectToSnowplowProspect(prospect, userAuth.username) + ); + + return updateProspectAsCurated(db, id); + }, + }, +}; diff --git a/servers/prospect-api/src/seeder.ts b/servers/prospect-api/src/seeder.ts new file mode 100644 index 00000000..28b37a74 --- /dev/null +++ b/servers/prospect-api/src/seeder.ts @@ -0,0 +1,80 @@ +import faker from '@faker-js/faker'; +import config from './config'; +import { + dbClient, + insertProspect, + Prospect, + ProspectType, + ScheduledSurface, + ScheduledSurfaces, + Topics, + truncateDb, +} from 'prospectapi-common'; +import { CorpusLanguage } from './types'; + +// conjure up double the batch size so we get variance +const prospectsPerCombo = config.app.prospectBatchSize * 2; +let prospect: Prospect; + +const buildProspect = ( + surfaceGuid: ScheduledSurface, + prospectType: ProspectType +): Prospect => { + const isSyndicated = faker.datatype.boolean(); + + // randomize number of authors + const authorCount = faker.datatype.number({ min: 1, max: 3 }); + const authors: string[] = []; + + for (let i = 0; i < authorCount; i++) { + authors.push(faker.name.findName()); + } + + return { + id: faker.datatype.uuid(), + prospectId: faker.datatype.uuid(), + scheduledSurfaceGuid: surfaceGuid.guid, + topic: faker.random.arrayElement(Object.values(Topics)), + prospectType, + url: faker.internet.url(), + saveCount: faker.datatype.number(), + rank: faker.datatype.number(), + // unix timestamp + // at 3:14:07 on january 19, 2038 GMT, this value will exceed the int32 limit 🙃 + createdAt: Math.floor(faker.date.recent().getTime() / 1000), + // below properties will be populated via client api/parser + domain: faker.internet.domainName(), + excerpt: faker.lorem.paragraph(), + imageUrl: faker.image.imageUrl(), + language: faker.random.arrayElement(Object.values(CorpusLanguage)), + publisher: faker.company.companyName(), + title: faker.lorem.sentence(), + isSyndicated, + // only potentially be a collection if it's *not* syndicated + isCollection: isSyndicated ? false : faker.datatype.boolean(), + authors: authors.join(','), + }; +}; + +const seed = async () => { + // clear the database first + await truncateDb(dbClient); + + // loop over each scheduled surface + ScheduledSurfaces.forEach((surface) => { + // loop over each prospect type for the surface + surface.prospectTypes.forEach(async (prospectType) => { + console.log(`inserting prospects for ${surface.guid}/${prospectType}...`); + + for (let i = 0; i < prospectsPerCombo; i++) { + // generate a prospect + prospect = buildProspect(surface, prospectType); + + // insert the prospect + await insertProspect(dbClient, prospect); + } + }); + }); +}; + +seed(); diff --git a/servers/prospect-api/src/server.ts b/servers/prospect-api/src/server.ts new file mode 100644 index 00000000..75c07780 --- /dev/null +++ b/servers/prospect-api/src/server.ts @@ -0,0 +1,75 @@ +import { ApolloServer } from '@apollo/server'; +import { Server } from 'http'; +import { buildSubgraphSchema } from '@apollo/subgraph'; +import { resolvers } from './resolvers'; +import { errorHandler, sentryPlugin } from '@pocket-tools/apollo-utils'; +import { + ApolloServerPluginLandingPageDisabled, + ApolloServerPluginInlineTraceDisabled, + ApolloServerPluginUsageReportingDisabled, +} from '@apollo/server/plugin/disabled'; +import { ApolloServerPluginInlineTrace } from '@apollo/server/plugin/inlineTrace'; +import { ApolloServerPluginDrainHttpServer } from '@apollo/server/plugin/drainHttpServer'; +import typeDefs from './typeDefs'; +import { AdminAPIUserContext } from './types'; + +/** + * Sets up and configures an ApolloServer for the application. + * every request + * @returns ApolloServer + * @param httpServer + * @param isTest indicates that we're running automated tests + */ +export function getServer( + httpServer: Server, + isTest?: boolean | false +): ApolloServer { + const defaultPlugins = [ + ...(isTest + ? [] + : [sentryPlugin, ApolloServerPluginDrainHttpServer({ httpServer })]), + ]; + const testPlugins = [ + ApolloServerPluginUsageReportingDisabled(), + ApolloServerPluginLandingPageDisabled(), + ApolloServerPluginInlineTraceDisabled(), + ]; + const prodPlugins = [ + ApolloServerPluginLandingPageDisabled(), + ApolloServerPluginInlineTrace(), + ]; + const nonProdPlugins = [ + ApolloServerPluginInlineTraceDisabled(), + // Usage reporting is enabled by default if you have APOLLO_KEY in your environment + ApolloServerPluginUsageReportingDisabled(), + ]; + + let plugins; + if (isTest) { + plugins = defaultPlugins.concat(testPlugins); + } else { + plugins = + process.env.NODE_ENV === 'production' + ? defaultPlugins.concat(prodPlugins) + : defaultPlugins.concat(nonProdPlugins); + } + + return new ApolloServer({ + schema: buildSubgraphSchema([{ typeDefs: typeDefs, resolvers: resolvers }]), + plugins, + formatError: errorHandler, + }); +} + +/** + * Create and start the apollo server. Required to await server.start() + * before applying middleware. + */ +export async function startApolloServer( + httpServer: Server, + isTest?: boolean | false +): Promise> { + const server = getServer(httpServer); + await server.start(); + return server; +} diff --git a/servers/prospect-api/src/test/admin-server/fragments.gql.ts b/servers/prospect-api/src/test/admin-server/fragments.gql.ts new file mode 100644 index 00000000..d0878265 --- /dev/null +++ b/servers/prospect-api/src/test/admin-server/fragments.gql.ts @@ -0,0 +1,26 @@ +import { gql } from 'graphql-tag'; + +export const ProspectData = gql` + fragment ProspectData on Prospect { + id + prospectId + scheduledSurfaceGuid + topic + prospectType + url + createdAt + imageUrl + publisher + domain + title + excerpt + language + saveCount + isSyndicated + isCollection + authors + approvedCorpusItem { + url + } + } +`; diff --git a/servers/prospect-api/src/test/admin-server/index.ts b/servers/prospect-api/src/test/admin-server/index.ts new file mode 100644 index 00000000..8872ea1e --- /dev/null +++ b/servers/prospect-api/src/test/admin-server/index.ts @@ -0,0 +1,27 @@ +import { getContext } from '../../context'; +import { AdminAPIUserContext } from '../../types'; +import express from 'express'; +import http from 'http'; +import { expressMiddleware } from '@apollo/server/express4'; +import { startApolloServer } from '../../server'; + +export const getTestServer = async (port) => { + // initialize express with exposed httpServer so that it may be + // provided to drain plugin for graceful shutdown. + const app = express(); + const httpServer = http.createServer(app); + + // JSON parser to enable POST body with JSON + app.use(express.json()); + + const apolloServer = await startApolloServer(httpServer, true); + + app.use( + expressMiddleware(apolloServer, { + context: async ({ req }) => getContext({ req }), + }) + ); + + await new Promise((resolve) => httpServer.listen({ port }, resolve)); + return { app, apolloServer, url: '/' }; +}; diff --git a/servers/prospect-api/src/test/admin-server/mutations.gql.ts b/servers/prospect-api/src/test/admin-server/mutations.gql.ts new file mode 100644 index 00000000..a617f2d0 --- /dev/null +++ b/servers/prospect-api/src/test/admin-server/mutations.gql.ts @@ -0,0 +1,26 @@ +import { gql } from 'graphql-tag'; +import { ProspectData } from './fragments.gql'; + +/** + * sample mutations for apollo server integration tests + */ +export const UPDATE_PROSPECT_AS_CURATED = gql` + mutation updateProspectAsCurated($id: ID!) { + updateProspectAsCurated(id: $id) { + ...ProspectData + } + } + ${ProspectData} +`; + +/** + * sample mutations for apollo server integration tests + */ +export const UPDATE_DISMISS_PROSPECT = gql` + mutation dismissProspect($id: ID!) { + dismissProspect(id: $id) { + ...ProspectData + } + } + ${ProspectData} +`; diff --git a/servers/prospect-api/src/test/admin-server/queries.gql.ts b/servers/prospect-api/src/test/admin-server/queries.gql.ts new file mode 100644 index 00000000..ee899b62 --- /dev/null +++ b/servers/prospect-api/src/test/admin-server/queries.gql.ts @@ -0,0 +1,14 @@ +import { gql } from 'graphql-tag'; +import { ProspectData } from './fragments.gql'; + +/** + * sample queries for apollo server integration tests + */ +export const GET_PROSPECTS = gql` + query getProspects($filters: GetProspectsFilters!) { + getProspects(filters: $filters) { + ...ProspectData + } + } + ${ProspectData} +`; diff --git a/servers/prospect-api/src/typeDefs.ts b/servers/prospect-api/src/typeDefs.ts new file mode 100644 index 00000000..a3840df1 --- /dev/null +++ b/servers/prospect-api/src/typeDefs.ts @@ -0,0 +1,7 @@ +import path from 'path'; +import fs from 'fs'; +import { gql } from 'graphql-tag'; + +export default gql( + fs.readFileSync(path.join(__dirname, '..', 'schema.graphql')).toString() +); diff --git a/servers/prospect-api/src/types.ts b/servers/prospect-api/src/types.ts new file mode 100644 index 00000000..18d310c4 --- /dev/null +++ b/servers/prospect-api/src/types.ts @@ -0,0 +1,69 @@ +import { DynamoDBDocumentClient } from '@aws-sdk/lib-dynamodb'; +import { CorpusLanguage, ProspectType, Prospect } from 'prospectapi-common'; +import { BaseContext } from '@apollo/server'; + +// Re-export CorpusLanguage, used to be part of this file, but moved to common +export { CorpusLanguage }; + +// this interface aligns with the graphql input type for getProspects query +export interface GetProspectsFilters { + filters: { scheduledSurfaceGuid: string; prospectType?: ProspectType }; +} + +// interface for ranked prospects grouped by a prospect type. key is one of ProspectType +export type SortedRankedProspects = { + [key: string]: Prospect[]; +}; + +// interface for when we create an auth object for a user in the graphql server context +export interface UserAuth { + name: string; + username: string; + groups: string | string[]; + hasReadOnly: boolean; + hasCuratorFull: boolean; + canRead: (scheduledSurfaceGuid: string) => boolean; + canWrite: (scheduledSurfaceGuid: string) => boolean; +} + +// enum to store access groups that align with congnito custom groups +export enum MozillaAccessGroup { + READONLY = 'team_pocket', // Read only access to all curation tools + COLLECTION_CURATOR_FULL = 'mozilliansorg_pocket_collection_curator_full', // Access to full collection tool + SCHEDULED_SURFACE_CURATOR_FULL = 'mozilliansorg_pocket_scheduled_surface_curator_full', // Access to full corpus tool, implies they have access to all scheduled surfaces. + NEW_TAB_CURATOR_ENUS = 'mozilliansorg_pocket_new_tab_curator_enus', // Access to en-us new tab in the corpus tool. + NEW_TAB_CURATOR_DEDE = 'mozilliansorg_pocket_new_tab_curator_dede', // Access to de-de new tab in corpus tool. + NEW_TAB_CURATOR_ENGB = 'mozilliansorg_pocket_new_tab_curator_engb', // Access to en-gb new tab in corpus tool. + NEW_TAB_CURATOR_ENINTL = 'mozilliansorg_pocket_new_tab_curator_enintl', // Access to en-intl new tab in corpus tool. + NEW_TAB_CURATOR_ESES = 'mozilliansorg_pocket_new_tab_curator_eses', // Access to es-es new tab in the corpus tool. + NEW_TAB_CURATOR_FRFR = 'mozilliansorg_pocket_new_tab_curator_frfr', // Access to fr-fr new tab in the corpus tool. + NEW_TAB_CURATOR_ITIT = 'mozilliansorg_pocket_new_tab_curator_itit', // Access to it-it new tab in the corpus tool. + POCKET_HITS_CURATOR_ENUS = 'mozilliansorg_pocket_pocket_hits_curator_enus', // Access to en us Pocket Hits in the corpus tool. + POCKET_HITS_CURATOR_DEDE = 'mozilliansorg_pocket_pocket_hits_curator_dede', // Access to de de Pocket Hits in the corpus tool. + CURATOR_SANDBOX = 'mozilliansorg_pocket_curator_sandbox', // Access to sandbox test surface in the corpus tool. +} + +// enum that maps the scheduled surface guid to a Mozilla access group +export enum ScheduledSurfaceGuidToMozillaAccessGroup { + NEW_TAB_EN_US = MozillaAccessGroup.NEW_TAB_CURATOR_ENUS, + NEW_TAB_EN_GB = MozillaAccessGroup.NEW_TAB_CURATOR_ENGB, + NEW_TAB_EN_INTL = MozillaAccessGroup.NEW_TAB_CURATOR_ENINTL, + NEW_TAB_DE_DE = MozillaAccessGroup.NEW_TAB_CURATOR_DEDE, + NEW_TAB_ES_ES = MozillaAccessGroup.NEW_TAB_CURATOR_ESES, + NEW_TAB_FR_FR = MozillaAccessGroup.NEW_TAB_CURATOR_FRFR, + NEW_TAB_IT_IT = MozillaAccessGroup.NEW_TAB_CURATOR_ITIT, + POCKET_HITS_EN_US = MozillaAccessGroup.POCKET_HITS_CURATOR_ENUS, + POCKET_HITS_DE_DE = MozillaAccessGroup.POCKET_HITS_CURATOR_DEDE, + SANDBOX = MozillaAccessGroup.CURATOR_SANDBOX, +} + +export interface AdminAPIUserContext extends BaseContext { + db: any; + userAuth: UserAuth; +} + +// type for the context object created when instantiating a new server +export class Context implements AdminAPIUserContext { + db: DynamoDBDocumentClient; + userAuth: UserAuth; +} diff --git a/servers/prospect-api/tsconfig.json b/servers/prospect-api/tsconfig.json new file mode 100644 index 00000000..50cc03f1 --- /dev/null +++ b/servers/prospect-api/tsconfig.json @@ -0,0 +1,15 @@ +{ + "extends": "tsconfig/graphql.json", + "compilerOptions": { + "outDir": "dist", + "rootDir": "src" + }, + "exclude": [ + "node_modules/", + "dist/" + ], + "include": [ + "src/**/*.ts", + ] +} + diff --git a/turbo.json b/turbo.json index 6d1dcc9f..9d95a41c 100644 --- a/turbo.json +++ b/turbo.json @@ -13,12 +13,19 @@ "test-integrations": { "dependsOn": ["^build"] }, + "synth": { + "dependsOn": ["^build"], + "outputs": ["cdktf.out/**"] + }, "lint": {}, "format": {}, "dev": { "cache": false, "persistent": true }, + "db:seed": { + "cache": false + }, "db:generate-client": { "cache": false }, From 2965d2a64c6d4ea5373e8e13bd52ab6b396dec36 Mon Sep 17 00:00:00 2001 From: Katerina Chinnappan Date: Wed, 10 Jan 2024 11:40:08 -0800 Subject: [PATCH 2/8] adding tf lock-timeout --- .circleci/shared.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.circleci/shared.yml b/.circleci/shared.yml index 54aad918..c0da592a 100644 --- a/.circleci/shared.yml +++ b/.circleci/shared.yml @@ -186,7 +186,7 @@ jobs: command: | . /home/circleci/.codebuild_shims_wrapper.sh cd << parameters.stack-output-path >> - /home/circleci/tfcmt --var target:<< parameters.scope >><<#parameters.dev>>-dev<> apply -- terraform apply -auto-approve + /home/circleci/tfcmt --var target:<< parameters.scope >><<#parameters.dev>>-dev<> apply -- terraform apply -auto-approve -lock-timeout=10m - when: condition: <> steps: @@ -208,7 +208,7 @@ jobs: command: | . /home/circleci/.codebuild_shims_wrapper.sh cd << parameters.stack-output-path >> - /home/circleci/tfcmt --var target:<< parameters.scope >><<#parameters.dev>>-dev<> plan --skip-no-changes --patch -- terraform plan + /home/circleci/tfcmt --var target:<< parameters.scope >><<#parameters.dev>>-dev<> plan --skip-no-changes --patch -- terraform plan -lock-timeout=10m - save_cache: key: tfenv-v2 paths: From e55291e53b8ba520a57a802a773ed6f775015688 Mon Sep 17 00:00:00 2001 From: Mathijs Miermans Date: Wed, 10 Jan 2024 11:50:23 -0800 Subject: [PATCH 3/8] feat(ProspectAPI): [MC-435] Add SLATE_SCHEDULER prospect type --- packages/prospectapi-common/types.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/prospectapi-common/types.ts b/packages/prospectapi-common/types.ts index 1713e933..e2f8ac25 100644 --- a/packages/prospectapi-common/types.ts +++ b/packages/prospectapi-common/types.ts @@ -49,6 +49,7 @@ export enum ProspectType { RSS_LOGISTIC_RECENT = 'RSS_LOGISTIC_RECENT', DISMISSED = 'DISMISSED', CONSTRAINT_SCHEDULE = 'CONSTRAINT_SCHEDULE', + SLATE_SCHEDULER = 'SLATE_SCHEDULER', } // languages we support in the corpus @@ -135,6 +136,7 @@ export const ScheduledSurfaces: ScheduledSurface[] = [ ProspectType.RSS_LOGISTIC, ProspectType.RSS_LOGISTIC_RECENT, ProspectType.CONSTRAINT_SCHEDULE, + ProspectType.SLATE_SCHEDULER, ], }, { From 7fc0c809afd8e771696c0c2fb96a3c1a11690e42 Mon Sep 17 00:00:00 2001 From: Katerina Chinnappan Date: Wed, 10 Jan 2024 12:04:59 -0800 Subject: [PATCH 4/8] adding pr template --- .github/PULL_REQUEST_TEMPLATE.md | 34 ++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 .github/PULL_REQUEST_TEMPLATE.md diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 00000000..11ed841f --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,34 @@ +## Goal + +What changed? What is the business/product goal? + +- Change 1 +- Change 2 + +## I'd love feedback/perspectives on: + +- + +## Implementation Decisions + +- + +## Deployment steps + +- [ ] Database migrations? +- [ ] Deployed to dev? +- [ ] Secrets? + +## References + +JIRA ticket: + +- Link to JIRA ticket + +Issue: + +- Link to GitHub issue + +Documentation: + +- Project doc From 3197156f75411f49798d7c1bbb70c23938562f90 Mon Sep 17 00:00:00 2001 From: Katerina Chinnappan Date: Wed, 10 Jan 2024 12:08:58 -0800 Subject: [PATCH 5/8] adding coverage step + codeowners --- .github/CODEOWNERS | 3 +++ .github/workflows/coverage.yml | 11 +++++++++++ 2 files changed, 14 insertions(+) create mode 100644 .github/CODEOWNERS create mode 100644 .github/workflows/coverage.yml diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 00000000..ebea3f36 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,3 @@ +# All Files/PRs + +* @MozillaSocial/content \ No newline at end of file diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml new file mode 100644 index 00000000..a25daf9a --- /dev/null +++ b/.github/workflows/coverage.yml @@ -0,0 +1,11 @@ +name: 'coverage' +on: + pull_request: + branches: + - main +jobs: + coverage: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: ArtiomTr/jest-coverage-report-action@v2 \ No newline at end of file From 2853689c82b005cff543ac50d897df1590094c7f Mon Sep 17 00:00:00 2001 From: Katerina Chinnappan Date: Wed, 10 Jan 2024 12:11:51 -0800 Subject: [PATCH 6/8] update coverage --- .github/workflows/coverage.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index a25daf9a..793962da 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -7,5 +7,5 @@ jobs: coverage: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - - uses: ArtiomTr/jest-coverage-report-action@v2 \ No newline at end of file + - uses: actions/checkout@v1 + - uses: ArtiomTr/jest-coverage-report-action@v2.0-rc.6 \ No newline at end of file From 289924c8f865965f3e1f1992c3845682f329e6bd Mon Sep 17 00:00:00 2001 From: Katerina Chinnappan Date: Wed, 10 Jan 2024 12:14:40 -0800 Subject: [PATCH 7/8] removing coverage for now --- .github/workflows/coverage.yml | 11 ----------- 1 file changed, 11 deletions(-) delete mode 100644 .github/workflows/coverage.yml diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml deleted file mode 100644 index 793962da..00000000 --- a/.github/workflows/coverage.yml +++ /dev/null @@ -1,11 +0,0 @@ -name: 'coverage' -on: - pull_request: - branches: - - main -jobs: - coverage: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v1 - - uses: ArtiomTr/jest-coverage-report-action@v2.0-rc.6 \ No newline at end of file From ce126f5cb514eb711ac62b799673221d896a41e7 Mon Sep 17 00:00:00 2001 From: Katerina Chinnappan Date: Wed, 10 Jan 2024 15:11:18 -0800 Subject: [PATCH 8/8] addressing feedback --- example.env | 4 ++-- servers/prospect-api/README.md | 13 ++++++------- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/example.env b/example.env index c24d8e36..f6dc9604 100644 --- a/example.env +++ b/example.env @@ -2,9 +2,9 @@ AWS_ACCESS_KEY_ID=localstack-fake-id AWS_DEFAULT_REGION=us-east-1 AWS_REGION=us-east-1 PROSPECT_API_PROSPECTS_TABLE=PROAPI-local-Prospects -AWS_ENDPOINT=http://localstack:4566 +AWS_ENDPOINT=http://localhost:4566 AWS_SECRET_ACCESS_KEY=localstack-fake-key LOCALSTACK_API_KEY=another-fake-key AWS_XRAY_CONTEXT_MISSING=LOG_ERROR AWS_XRAY_LOG_LEVEL=silent -SNOWPLOW_ENDPOINT=snowplow:9090 \ No newline at end of file +SNOWPLOW_ENDPOINT=localhost:9090 \ No newline at end of file diff --git a/servers/prospect-api/README.md b/servers/prospect-api/README.md index 26ebe69f..d33d4fc7 100644 --- a/servers/prospect-api/README.md +++ b/servers/prospect-api/README.md @@ -1,3 +1,4 @@ +*Note: README needs some updated as service is migrated to monorepo* # Prospect API This repo contains two distinct but related applications: an API and a Lambda. @@ -96,16 +97,14 @@ To run operations in the GraphQL playground, you'll need to specify some HTTP he 1. Open up the GraphQL playground at `http://localhost:4026`. 2. Click the **HTTP HEADERS** link at the bottom of the left hand side of the playground to reveal a text box. -3. Enter the necessary headers (see sample below) into the box and try an operation - it should work! +3. Click the 'Bulk Edit' button at the bottom of the screen and paste in the text below The sample headers below allow full access to all queries and mutations: -```typescript -{ - "groups": "mozilliansorg_pocket_scheduled_surface_curator_full", - "name": "Cherry Glazer", - "username": "ad|Mozilla-LDAP|cglazer" -} +``` +groups:mozilliansorg_pocket_scheduled_surface_curator_full +name:Cherry Glazer +username:ad|Mozilla-LDAP|cglazer ``` Note that the `groups` header can contain mulitple values separated by commas (but still in a single string).