From 76ec9a1598e5f395a37c75c9863a64c52ab596c3 Mon Sep 17 00:00:00 2001 From: suhussai Date: Mon, 11 Mar 2024 14:12:24 -0600 Subject: [PATCH] feat: add billing interfaces and firehose ingestor aggregator (#7) * feat: add billing interfaces and firehose ingestor aggregator * refactor EventManager and update as per PR comments * describe test string update * add jsdocs * update API.md * fix delete tenant bug * combine IBilling, BillingProvider imports into one line --- .gitignore | 1 + .projen/deps.json | 24 +- .projen/tasks.json | 4 +- .projenrc.ts | 23 +- API.md | 1190 +++++++++++++---- package-lock.json | 61 +- package.json | 9 +- resources/functions/data-aggregator/index.py | 61 + .../models/control_plane_event_types.py | 14 - .../tenant_management.py | 89 +- .../models/control_plane_event_types.py | 14 - .../billing/billing-interface.ts | 42 + src/control-plane/billing/billing-provider.ts | 87 ++ src/control-plane/billing/index.ts | 5 + src/control-plane/control-plane-api.ts | 20 +- src/control-plane/control-plane.ts | 122 +- src/control-plane/index.ts | 2 + .../firehose-ingestor-aggregator.ts | 198 +++ .../ingestor-aggregator/index.ts | 5 + .../ingestor-aggregator-interface.ts | 26 + src/control-plane/integ.default.ts | 15 +- src/control-plane/services.ts | 30 +- .../tenant-config/tenant-config-service.ts | 4 +- src/core-app-plane/bash-job-orchestrator.ts | 16 +- src/core-app-plane/bash-job-runner.ts | 32 +- src/core-app-plane/core-app-plane.ts | 76 +- src/core-app-plane/integ.default.ts | 194 ++- src/utils/event-manager.ts | 131 +- test/control-plane.test.ts | 27 +- test/core-app-plane.test.ts | 37 +- 30 files changed, 1932 insertions(+), 627 deletions(-) create mode 100644 resources/functions/data-aggregator/index.py delete mode 100644 resources/functions/models/control_plane_event_types.py rename resources/functions/{ => tenant-management}/tenant_management.py (62%) delete mode 100644 resources/layers/models/control_plane_event_types.py create mode 100644 src/control-plane/billing/billing-interface.ts create mode 100644 src/control-plane/billing/billing-provider.ts create mode 100644 src/control-plane/billing/index.ts create mode 100644 src/control-plane/ingestor-aggregator/firehose-ingestor-aggregator.ts create mode 100644 src/control-plane/ingestor-aggregator/index.ts create mode 100644 src/control-plane/ingestor-aggregator/ingestor-aggregator-interface.ts diff --git a/.gitignore b/.gitignore index 91dcc44..fd4db23 100644 --- a/.gitignore +++ b/.gitignore @@ -37,6 +37,7 @@ __pycache__/ !.ort.yml .idea .vscode +cdk.out /test-reports/ junit.xml /coverage/ diff --git a/.projen/deps.json b/.projen/deps.json index 1a720a8..e47e29c 100644 --- a/.projen/deps.json +++ b/.projen/deps.json @@ -1,5 +1,10 @@ { "dependencies": [ + { + "name": "@aws-cdk/aws-kinesisfirehose-alpha", + "version": "2.123.0-alpha.0", + "type": "build" + }, { "name": "@types/jest", "type": "build" @@ -19,6 +24,11 @@ "version": "^6", "type": "build" }, + { + "name": "aws-cdk", + "version": "2.123.0", + "type": "build" + }, { "name": "eslint-config-prettier", "type": "build" @@ -101,9 +111,13 @@ "name": "typescript", "type": "build" }, + { + "name": "@aws-cdk/aws-kinesisfirehose-alpha", + "type": "peer" + }, { "name": "aws-cdk-lib", - "version": "^2.114.1", + "version": "^2.123.0", "type": "peer" }, { @@ -111,6 +125,14 @@ "version": "^10.0.5", "type": "peer" }, + { + "name": "@aws-cdk/aws-kinesisfirehose-alpha", + "type": "runtime" + }, + { + "name": "@aws-cdk/aws-kinesisfirehose-destinations-alpha", + "type": "runtime" + }, { "name": "@aws-cdk/aws-lambda-python-alpha", "type": "runtime" diff --git a/.projen/tasks.json b/.projen/tasks.json index 16e65cf..4815895 100644 --- a/.projen/tasks.json +++ b/.projen/tasks.json @@ -263,13 +263,13 @@ }, "steps": [ { - "exec": "npx npm-check-updates@16 --upgrade --target=minor --peer --dep=dev,peer,prod,optional --filter=@types/jest,eslint-config-prettier,eslint-import-resolver-typescript,eslint-plugin-header,eslint-plugin-import,eslint-plugin-prettier,jest,jsii-diff,jsii-docgen,jsii-pacmak,jsii-rosetta,jsii,prettier,projen,ts-jest,ts-node,typescript,@aws-cdk/aws-lambda-python-alpha,cdk-nag" + "exec": "npx npm-check-updates@16 --upgrade --target=minor --peer --dep=dev,peer,prod,optional --filter=@aws-cdk/aws-kinesisfirehose-alpha,@types/jest,aws-cdk,eslint-config-prettier,eslint-import-resolver-typescript,eslint-plugin-header,eslint-plugin-import,eslint-plugin-prettier,jest,jsii-diff,jsii-docgen,jsii-pacmak,jsii-rosetta,jsii,prettier,projen,ts-jest,ts-node,typescript,@aws-cdk/aws-kinesisfirehose-destinations-alpha,@aws-cdk/aws-lambda-python-alpha,cdk-nag" }, { "exec": "npm install" }, { - "exec": "npm update @types/jest @types/node @typescript-eslint/eslint-plugin @typescript-eslint/parser eslint-config-prettier eslint-import-resolver-typescript eslint-plugin-header eslint-plugin-import eslint-plugin-prettier eslint jest jest-junit jsii-diff jsii-docgen jsii-pacmak jsii-rosetta jsii prettier projen standard-version ts-jest ts-node typescript aws-cdk-lib constructs @aws-cdk/aws-lambda-python-alpha cdk-nag" + "exec": "npm update @aws-cdk/aws-kinesisfirehose-alpha @types/jest @types/node @typescript-eslint/eslint-plugin @typescript-eslint/parser aws-cdk eslint-config-prettier eslint-import-resolver-typescript eslint-plugin-header eslint-plugin-import eslint-plugin-prettier eslint jest jest-junit jsii-diff jsii-docgen jsii-pacmak jsii-rosetta jsii prettier projen standard-version ts-jest ts-node typescript aws-cdk-lib constructs @aws-cdk/aws-kinesisfirehose-destinations-alpha @aws-cdk/aws-lambda-python-alpha cdk-nag" }, { "exec": "npx projen" diff --git a/.projenrc.ts b/.projenrc.ts index 8f2b786..017bd87 100644 --- a/.projenrc.ts +++ b/.projenrc.ts @@ -5,11 +5,11 @@ import { awscdk, javascript } from 'projen'; import { GithubCredentials } from 'projen/lib/github'; import { NpmAccess } from 'projen/lib/javascript'; -const GITHUB_USER = 'awslabs'; -const PUBLICATION_NAMESPACE = 'cdklabs'; -const PROJECT_NAME = 'sbt-aws'; -const CDK_VERSION: string = '2.114.1'; +const GITHUB_USER: string = 'awslabs'; +const PUBLICATION_NAMESPACE: string = 'cdklabs'; +const PROJECT_NAME: string = 'sbt-aws'; const PROJEN_VERSION: string = '0.80.2'; +const CDK_VERSION: string = '2.123.0'; const project = new awscdk.AwsCdkConstructLibrary({ author: 'Amazon Web Services - SaaS Factory', @@ -20,10 +20,19 @@ const project = new awscdk.AwsCdkConstructLibrary({ copyrightOwner: 'Amazon.com, Inc. or its affiliates. All Rights Reserved.', copyrightPeriod: '2024-', defaultReleaseBranch: 'main', - deps: ['@aws-cdk/aws-lambda-python-alpha', 'cdk-nag'], + deps: [ + '@aws-cdk/aws-lambda-python-alpha', + 'cdk-nag', + '@aws-cdk/aws-kinesisfirehose-alpha', + '@aws-cdk/aws-kinesisfirehose-destinations-alpha', + ], description: 'SaaS Builder Toolkit for AWS is a developer toolkit to implement SaaS best practices and increase developer velocity.', - devDeps: ['eslint-plugin-header'], + devDeps: [ + `aws-cdk@${CDK_VERSION}`, + 'eslint-plugin-header', + `@aws-cdk/aws-kinesisfirehose-alpha@${CDK_VERSION}-alpha.0`, + ], github: true, jsiiVersion: '~5.2.0', keywords: ['constructs', 'aws-cdk', 'saas'], @@ -34,6 +43,7 @@ const project = new awscdk.AwsCdkConstructLibrary({ name: `@${PUBLICATION_NAMESPACE}/${PROJECT_NAME}`, npmignoreEnabled: true, packageManager: javascript.NodePackageManager.NPM, + peerDeps: ['@aws-cdk/aws-kinesisfirehose-alpha'], prettier: true, projenrcTs: true, projenVersion: PROJEN_VERSION, @@ -90,6 +100,7 @@ const project = new awscdk.AwsCdkConstructLibrary({ '!.ort.yml', '.idea', '.vscode', + 'cdk.out', ], }); diff --git a/API.md b/API.md index 31507ab..16f7b4e 100644 --- a/API.md +++ b/API.md @@ -216,7 +216,7 @@ Any object. | node | constructs.Node | The tree node. | | codebuildProject | aws-cdk-lib.aws_codebuild.Project | The codebuildProject used to implement this BashJobRunner. | | eventTarget | aws-cdk-lib.aws_events.IRuleTarget | The eventTarget to use when triggering this BashJobRunner. | -| exportedVariables | string[] | The environment variables to export into the outgoing event once the BashJobRunner has finished. | +| environmentVariablesToOutgoingEvent | string[] | The environment variables to export into the outgoing event once the BashJobRunner has finished. | --- @@ -256,10 +256,10 @@ The eventTarget to use when triggering this BashJobRunner. --- -##### `exportedVariables`Optional +##### `environmentVariablesToOutgoingEvent`Optional ```typescript -public readonly exportedVariables: string[]; +public readonly environmentVariablesToOutgoingEvent: string[]; ``` - *Type:* string[] @@ -269,6 +269,120 @@ The environment variables to export into the outgoing event once the BashJobRunn --- +### BillingProvider + +#### Initializers + +```typescript +import { BillingProvider } from '@cdklabs/sbt-aws' + +new BillingProvider(scope: Construct, id: string, props: BillingProviderProps) +``` + +| **Name** | **Type** | **Description** | +| --- | --- | --- | +| scope | constructs.Construct | *No description.* | +| id | string | *No description.* | +| props | BillingProviderProps | *No description.* | + +--- + +##### `scope`Required + +- *Type:* constructs.Construct + +--- + +##### `id`Required + +- *Type:* string + +--- + +##### `props`Required + +- *Type:* BillingProviderProps + +--- + +#### Methods + +| **Name** | **Description** | +| --- | --- | +| toString | Returns a string representation of this construct. | + +--- + +##### `toString` + +```typescript +public toString(): string +``` + +Returns a string representation of this construct. + +#### Static Functions + +| **Name** | **Description** | +| --- | --- | +| isConstruct | Checks if `x` is a construct. | + +--- + +##### ~~`isConstruct`~~ + +```typescript +import { BillingProvider } from '@cdklabs/sbt-aws' + +BillingProvider.isConstruct(x: any) +``` + +Checks if `x` is a construct. + +###### `x`Required + +- *Type:* any + +Any object. + +--- + +#### Properties + +| **Name** | **Type** | **Description** | +| --- | --- | --- | +| node | constructs.Node | The tree node. | +| controlPlaneAPIBillingWebhookResource | aws-cdk-lib.aws_apigateway.IResource | The API Gateway resource containing the billing webhook resource. | + +--- + +##### `node`Required + +```typescript +public readonly node: Node; +``` + +- *Type:* constructs.Node + +The tree node. + +--- + +##### `controlPlaneAPIBillingWebhookResource`Optional + +```typescript +public readonly controlPlaneAPIBillingWebhookResource: IResource; +``` + +- *Type:* aws-cdk-lib.aws_apigateway.IResource + +The API Gateway resource containing the billing webhook resource. + +Only set when the IBilling webhookFunction is defined. + +--- + + ### CognitoAuth - *Implements:* IAuth @@ -586,10 +700,9 @@ Any object. | --- | --- | --- | | node | constructs.Node | The tree node. | | controlPlaneAPIGatewayUrl | string | *No description.* | -| controlPlaneSource | string | *No description.* | | eventBusArn | string | *No description.* | -| offboardingDetailType | string | *No description.* | -| onboardingDetailType | string | *No description.* | +| eventManager | EventManager | *No description.* | +| tables | Tables | *No description.* | --- @@ -615,16 +728,6 @@ public readonly controlPlaneAPIGatewayUrl: string; --- -##### `controlPlaneSource`Required - -```typescript -public readonly controlPlaneSource: string; -``` - -- *Type:* string - ---- - ##### `eventBusArn`Required ```typescript @@ -635,23 +738,23 @@ public readonly eventBusArn: string; --- -##### `offboardingDetailType`Required +##### `eventManager`Required ```typescript -public readonly offboardingDetailType: string; +public readonly eventManager: EventManager; ``` -- *Type:* string +- *Type:* EventManager --- -##### `onboardingDetailType`Required +##### `tables`Required ```typescript -public readonly onboardingDetailType: string; +public readonly tables: Tables; ``` -- *Type:* string +- *Type:* Tables --- @@ -739,6 +842,7 @@ Any object. | **Name** | **Type** | **Description** | | --- | --- | --- | | node | constructs.Node | The tree node. | +| billingResource | aws-cdk-lib.aws_apigateway.Resource | *No description.* | | tenantUpdateServiceTarget | aws-cdk-lib.aws_events_targets.ApiGateway | *No description.* | | apiUrl | any | *No description.* | @@ -756,6 +860,16 @@ The tree node. --- +##### `billingResource`Required + +```typescript +public readonly billingResource: Resource; +``` + +- *Type:* aws-cdk-lib.aws_apigateway.Resource + +--- + ##### `tenantUpdateServiceTarget`Required ```typescript @@ -866,6 +980,7 @@ Any object. | **Name** | **Type** | **Description** | | --- | --- | --- | | node | constructs.Node | The tree node. | +| eventManager | EventManager | *No description.* | --- @@ -881,6 +996,16 @@ The tree node. --- +##### `eventManager`Required + +```typescript +public readonly eventManager: EventManager; +``` + +- *Type:* EventManager + +--- + ### EventManager @@ -925,7 +1050,7 @@ new EventManager(scope: Construct, id: string, props: EventManagerProps) | **Name** | **Description** | | --- | --- | | toString | Returns a string representation of this construct. | -| addRuleWithTarget | Function to add a new rule and register a target for the newly added rule. | +| addTargetToEvent | Adds an IRuleTarget to an event. | --- @@ -937,36 +1062,28 @@ public toString(): string Returns a string representation of this construct. -##### `addRuleWithTarget` +##### `addTargetToEvent` ```typescript -public addRuleWithTarget(ruleName: string, eventDetailType: string[], eventSource: string[], target: IRuleTarget): void +public addTargetToEvent(eventType: DetailType, target: IRuleTarget): void ``` -Function to add a new rule and register a target for the newly added rule. - -###### `ruleName`Required - -- *Type:* string - ---- - -###### `eventDetailType`Required +Adds an IRuleTarget to an event. -- *Type:* string[] - ---- +###### `eventType`Required -###### `eventSource`Required +- *Type:* DetailType -- *Type:* string[] +The name of the event to add a target to. --- -###### `target`Required +###### `target`Required - *Type:* aws-cdk-lib.aws_events.IRuleTarget +The target that will be added to the event. + --- #### Static Functions @@ -1000,6 +1117,10 @@ Any object. | **Name** | **Type** | **Description** | | --- | --- | --- | | node | constructs.Node | The tree node. | +| applicationPlaneEventSource | string | *No description.* | +| controlPlaneEventSource | string | *No description.* | +| eventBus | aws-cdk-lib.aws_events.IEventBus | The event bus to register new rules with. | +| supportedEvents | {[ key: string ]: string} | *No description.* | --- @@ -1015,45 +1136,101 @@ The tree node. --- +##### `applicationPlaneEventSource`Required -### LambdaLayers +```typescript +public readonly applicationPlaneEventSource: string; +``` -#### Initializers +- *Type:* string + +--- + +##### `controlPlaneEventSource`Required ```typescript -import { LambdaLayers } from '@cdklabs/sbt-aws' +public readonly controlPlaneEventSource: string; +``` -new LambdaLayers(scope: Construct, id: string) +- *Type:* string + +--- + +##### `eventBus`Required + +```typescript +public readonly eventBus: IEventBus; +``` + +- *Type:* aws-cdk-lib.aws_events.IEventBus + +The event bus to register new rules with. + +--- + +##### `supportedEvents`Required + +```typescript +public readonly supportedEvents: {[ key: string ]: string}; +``` + +- *Type:* {[ key: string ]: string} + +--- + + +### FirehoseAggregator + +- *Implements:* IDataIngestorAggregator + +Creates a Kinesis Firehose to accept high-volume data, which it then routes to an s3 bucket. + +The s3 bucket triggers a lambda which processes the data and stores it in a DynamoDB table +containing the aggregated data. + +#### Initializers + +```typescript +import { FirehoseAggregator } from '@cdklabs/sbt-aws' + +new FirehoseAggregator(scope: Construct, id: string, props: FirehoseAggregatorProps) ``` | **Name** | **Type** | **Description** | | --- | --- | --- | -| scope | constructs.Construct | *No description.* | -| id | string | *No description.* | +| scope | constructs.Construct | *No description.* | +| id | string | *No description.* | +| props | FirehoseAggregatorProps | *No description.* | --- -##### `scope`Required +##### `scope`Required - *Type:* constructs.Construct --- -##### `id`Required +##### `id`Required - *Type:* string --- +##### `props`Required + +- *Type:* FirehoseAggregatorProps + +--- + #### Methods | **Name** | **Description** | | --- | --- | -| toString | Returns a string representation of this construct. | +| toString | Returns a string representation of this construct. | --- -##### `toString` +##### `toString` ```typescript public toString(): string @@ -1065,21 +1242,21 @@ Returns a string representation of this construct. | **Name** | **Description** | | --- | --- | -| isConstruct | Checks if `x` is a construct. | +| isConstruct | Checks if `x` is a construct. | --- -##### ~~`isConstruct`~~ +##### ~~`isConstruct`~~ ```typescript -import { LambdaLayers } from '@cdklabs/sbt-aws' +import { FirehoseAggregator } from '@cdklabs/sbt-aws' -LambdaLayers.isConstruct(x: any) +FirehoseAggregator.isConstruct(x: any) ``` Checks if `x` is a construct. -###### `x`Required +###### `x`Required - *Type:* any @@ -1091,12 +1268,15 @@ Any object. | **Name** | **Type** | **Description** | | --- | --- | --- | -| node | constructs.Node | The tree node. | -| controlPlaneLambdaLayer | aws-cdk-lib.aws_lambda.LayerVersion | *No description.* | +| node | constructs.Node | The tree node. | +| dataAggregator | aws-cdk-lib.aws_lambda.IFunction | The Python Lambda function responsible for aggregating the raw data coming in via the dataIngestor. | +| dataIngestor | @aws-cdk/aws-kinesisfirehose-alpha.DeliveryStream | The Firehose DeliveryStream ingestor responsible for accepting the incoming data. | +| dataIngestorName | string | The name of the dataIngestor. | +| dataRepository | aws-cdk-lib.aws_dynamodb.ITable | The DynamoDB table containing the aggregated data. | --- -##### `node`Required +##### `node`Required ```typescript public readonly node: Node; @@ -1108,41 +1288,81 @@ The tree node. --- -##### `controlPlaneLambdaLayer`Required +##### `dataAggregator`Required ```typescript -public readonly controlPlaneLambdaLayer: LayerVersion; +public readonly dataAggregator: IFunction; ``` -- *Type:* aws-cdk-lib.aws_lambda.LayerVersion +- *Type:* aws-cdk-lib.aws_lambda.IFunction + +The Python Lambda function responsible for aggregating the raw data coming in via the dataIngestor. --- +##### `dataIngestor`Required -### Messaging +```typescript +public readonly dataIngestor: DeliveryStream; +``` -#### Initializers +- *Type:* @aws-cdk/aws-kinesisfirehose-alpha.DeliveryStream + +The Firehose DeliveryStream ingestor responsible for accepting the incoming data. + +--- + +##### `dataIngestorName`Required ```typescript -import { Messaging } from '@cdklabs/sbt-aws' +public readonly dataIngestorName: string; +``` -new Messaging(scope: Construct, id: string) +- *Type:* string + +The name of the dataIngestor. + +This is used for visibility. + +--- + +##### `dataRepository`Required + +```typescript +public readonly dataRepository: ITable; +``` + +- *Type:* aws-cdk-lib.aws_dynamodb.ITable + +The DynamoDB table containing the aggregated data. + +--- + + +### LambdaLayers + +#### Initializers + +```typescript +import { LambdaLayers } from '@cdklabs/sbt-aws' + +new LambdaLayers(scope: Construct, id: string) ``` | **Name** | **Type** | **Description** | | --- | --- | --- | -| scope | constructs.Construct | *No description.* | -| id | string | *No description.* | +| scope | constructs.Construct | *No description.* | +| id | string | *No description.* | --- -##### `scope`Required +##### `scope`Required - *Type:* constructs.Construct --- -##### `id`Required +##### `id`Required - *Type:* string @@ -1152,11 +1372,11 @@ new Messaging(scope: Construct, id: string) | **Name** | **Description** | | --- | --- | -| toString | Returns a string representation of this construct. | +| toString | Returns a string representation of this construct. | --- -##### `toString` +##### `toString` ```typescript public toString(): string @@ -1168,21 +1388,21 @@ Returns a string representation of this construct. | **Name** | **Description** | | --- | --- | -| isConstruct | Checks if `x` is a construct. | +| isConstruct | Checks if `x` is a construct. | --- -##### ~~`isConstruct`~~ +##### ~~`isConstruct`~~ ```typescript -import { Messaging } from '@cdklabs/sbt-aws' +import { LambdaLayers } from '@cdklabs/sbt-aws' -Messaging.isConstruct(x: any) +LambdaLayers.isConstruct(x: any) ``` Checks if `x` is a construct. -###### `x`Required +###### `x`Required - *Type:* any @@ -1194,12 +1414,12 @@ Any object. | **Name** | **Type** | **Description** | | --- | --- | --- | -| node | constructs.Node | The tree node. | -| eventBus | aws-cdk-lib.aws_events.EventBus | *No description.* | +| node | constructs.Node | The tree node. | +| controlPlaneLambdaLayer | aws-cdk-lib.aws_lambda.LayerVersion | *No description.* | --- -##### `node`Required +##### `node`Required ```typescript public readonly node: Node; @@ -1211,7 +1431,110 @@ The tree node. --- -##### `eventBus`Required +##### `controlPlaneLambdaLayer`Required + +```typescript +public readonly controlPlaneLambdaLayer: LayerVersion; +``` + +- *Type:* aws-cdk-lib.aws_lambda.LayerVersion + +--- + + +### Messaging + +#### Initializers + +```typescript +import { Messaging } from '@cdklabs/sbt-aws' + +new Messaging(scope: Construct, id: string) +``` + +| **Name** | **Type** | **Description** | +| --- | --- | --- | +| scope | constructs.Construct | *No description.* | +| id | string | *No description.* | + +--- + +##### `scope`Required + +- *Type:* constructs.Construct + +--- + +##### `id`Required + +- *Type:* string + +--- + +#### Methods + +| **Name** | **Description** | +| --- | --- | +| toString | Returns a string representation of this construct. | + +--- + +##### `toString` + +```typescript +public toString(): string +``` + +Returns a string representation of this construct. + +#### Static Functions + +| **Name** | **Description** | +| --- | --- | +| isConstruct | Checks if `x` is a construct. | + +--- + +##### ~~`isConstruct`~~ + +```typescript +import { Messaging } from '@cdklabs/sbt-aws' + +Messaging.isConstruct(x: any) +``` + +Checks if `x` is a construct. + +###### `x`Required + +- *Type:* any + +Any object. + +--- + +#### Properties + +| **Name** | **Type** | **Description** | +| --- | --- | --- | +| node | constructs.Node | The tree node. | +| eventBus | aws-cdk-lib.aws_events.EventBus | *No description.* | + +--- + +##### `node`Required + +```typescript +public readonly node: Node; +``` + +- *Type:* constructs.Node + +The tree node. + +--- + +##### `eventBus`Required ```typescript public readonly eventBus: EventBus; @@ -1621,8 +1944,9 @@ const bashJobOrchestratorProps: BashJobOrchestratorProps = { ... } | detailType | string | The detail type to use when publishing event bridge events. | | eventSource | string | The event source to use when publishing event bridge events. | | targetEventBus | aws-cdk-lib.aws_events.IEventBus | The event bus to publish the outgoing event to. | -| exportedVariables | string[] | Environment variables to export into the outgoing event once the bash job has finished. | -| importedVariables | string[] | Environment variables to import into the bash job from event details field. | +| environmentJSONVariablesFromIncomingEvent | string[] | *No description.* | +| environmentStringVariablesFromIncomingEvent | string[] | Environment variables to import into the bash job from event details field. | +| environmentVariablesToOutgoingEvent | string[] | Environment variables to export into the outgoing event once the bash job has finished. | --- @@ -1884,22 +2208,20 @@ The event bus to publish the outgoing event to. --- -##### `exportedVariables`Optional +##### `environmentJSONVariablesFromIncomingEvent`Optional ```typescript -public readonly exportedVariables: string[]; +public readonly environmentJSONVariablesFromIncomingEvent: string[]; ``` - *Type:* string[] -Environment variables to export into the outgoing event once the bash job has finished. - --- -##### `importedVariables`Optional +##### `environmentStringVariablesFromIncomingEvent`Optional ```typescript -public readonly importedVariables: string[]; +public readonly environmentStringVariablesFromIncomingEvent: string[]; ``` - *Type:* string[] @@ -1908,6 +2230,18 @@ Environment variables to import into the bash job from event details field. --- +##### `environmentVariablesToOutgoingEvent`Optional + +```typescript +public readonly environmentVariablesToOutgoingEvent: string[]; +``` + +- *Type:* string[] + +Environment variables to export into the outgoing event once the bash job has finished. + +--- + ### BashJobRunnerProps Encapsulates the list of properties for a BashJobRunner. @@ -1924,138 +2258,157 @@ const bashJobRunnerProps: BashJobRunnerProps = { ... } | **Name** | **Type** | **Description** | | --- | --- | --- | -| eventBus | aws-cdk-lib.aws_events.IEventBus | The eventBus to submit the outgoing event to once the BashJobRunner has finished. | | name | string | The name of the BashJobRunner. | -| outgoingEventDetailType | string | The detail type of the event that will be emitted once the BashJobRunner has finished. | -| outgoingEventSource | string | The source of the event that will be emitted once the BashJobRunner has finished. | | permissions | aws-cdk-lib.aws_iam.PolicyDocument | The IAM permission document for the BashJobRunner. | | script | string | The bash script to run as part of the BashJobRunner. | -| exportedVariables | string[] | The environment variables to export into the outgoing event once the BashJobRunner has finished. | -| importedVariables | string[] | The environment variables to import into the BashJobRunner from event details field. | +| environmentVariablesFromIncomingEvent | string[] | The environment variables to import into the BashJobRunner from event details field. | +| environmentVariablesToOutgoingEvent | string[] | The environment variables to export into the outgoing event once the BashJobRunner has finished. | | postScript | string | The bash script to run after the main script has completed. | | scriptEnvironmentVariables | {[ key: string ]: string} | The variables to pass into the codebuild BashJobRunner. | --- -##### `eventBus`Required +##### `name`Required ```typescript -public readonly eventBus: IEventBus; +public readonly name: string; ``` -- *Type:* aws-cdk-lib.aws_events.IEventBus +- *Type:* string -The eventBus to submit the outgoing event to once the BashJobRunner has finished. +The name of the BashJobRunner. + +Note that this value must be unique. --- -##### `name`Required +##### `permissions`Required ```typescript -public readonly name: string; +public readonly permissions: PolicyDocument; ``` -- *Type:* string - -The name of the BashJobRunner. +- *Type:* aws-cdk-lib.aws_iam.PolicyDocument -Note that this value must be unique. +The IAM permission document for the BashJobRunner. --- -##### `outgoingEventDetailType`Required +##### `script`Required ```typescript -public readonly outgoingEventDetailType: string; +public readonly script: string; ``` - *Type:* string -The detail type of the event that will be emitted once the BashJobRunner has finished. +The bash script to run as part of the BashJobRunner. --- -##### `outgoingEventSource`Required +##### `environmentVariablesFromIncomingEvent`Optional ```typescript -public readonly outgoingEventSource: string; +public readonly environmentVariablesFromIncomingEvent: string[]; ``` -- *Type:* string +- *Type:* string[] -The source of the event that will be emitted once the BashJobRunner has finished. +The environment variables to import into the BashJobRunner from event details field. --- -##### `permissions`Required +##### `environmentVariablesToOutgoingEvent`Optional ```typescript -public readonly permissions: PolicyDocument; +public readonly environmentVariablesToOutgoingEvent: string[]; ``` -- *Type:* aws-cdk-lib.aws_iam.PolicyDocument +- *Type:* string[] -The IAM permission document for the BashJobRunner. +The environment variables to export into the outgoing event once the BashJobRunner has finished. --- -##### `script`Required +##### `postScript`Optional ```typescript -public readonly script: string; +public readonly postScript: string; ``` - *Type:* string -The bash script to run as part of the BashJobRunner. +The bash script to run after the main script has completed. --- -##### `exportedVariables`Optional +##### `scriptEnvironmentVariables`Optional ```typescript -public readonly exportedVariables: string[]; +public readonly scriptEnvironmentVariables: {[ key: string ]: string}; ``` -- *Type:* string[] +- *Type:* {[ key: string ]: string} -The environment variables to export into the outgoing event once the BashJobRunner has finished. +The variables to pass into the codebuild BashJobRunner. + +--- + +### BillingProviderProps + +Encapsulates the list of properties for a BillingProvider. + +#### Initializer + +```typescript +import { BillingProviderProps } from '@cdklabs/sbt-aws' + +const billingProviderProps: BillingProviderProps = { ... } +``` + +#### Properties + +| **Name** | **Type** | **Description** | +| --- | --- | --- | +| billing | IBilling | An implementation of the IBilling interface. | +| controlPlaneAPIBillingResource | aws-cdk-lib.aws_apigateway.Resource | An API Gateway Resource for the BillingProvider to use when setting up API endpoints. | +| eventManager | EventManager | An EventManager object to help coordinate events. | --- -##### `importedVariables`Optional +##### `billing`Required ```typescript -public readonly importedVariables: string[]; +public readonly billing: IBilling; ``` -- *Type:* string[] +- *Type:* IBilling -The environment variables to import into the BashJobRunner from event details field. +An implementation of the IBilling interface. --- -##### `postScript`Optional +##### `controlPlaneAPIBillingResource`Required ```typescript -public readonly postScript: string; +public readonly controlPlaneAPIBillingResource: Resource; ``` -- *Type:* string +- *Type:* aws-cdk-lib.aws_apigateway.Resource -The bash script to run after the main script has completed. +An API Gateway Resource for the BillingProvider to use when setting up API endpoints. --- -##### `scriptEnvironmentVariables`Optional +##### `eventManager`Required ```typescript -public readonly scriptEnvironmentVariables: {[ key: string ]: string}; +public readonly eventManager: EventManager; ``` -- *Type:* {[ key: string ]: string} +- *Type:* EventManager -The variables to pass into the codebuild BashJobRunner. +An EventManager object to help coordinate events. --- @@ -2184,22 +2537,11 @@ const controlPlaneProps: ControlPlaneProps = { ... } | **Name** | **Type** | **Description** | | --- | --- | --- | -| applicationPlaneEventSource | string | *No description.* | | auth | IAuth | *No description.* | -| controlPlaneEventSource | string | *No description.* | -| offboardingDetailType | string | *No description.* | -| onboardingDetailType | string | *No description.* | -| provisioningDetailType | string | *No description.* | - ---- - -##### `applicationPlaneEventSource`Required - -```typescript -public readonly applicationPlaneEventSource: string; -``` - -- *Type:* string +| applicationPlaneEventSource | string | The source to use for outgoing events that will be placed on the EventBus. | +| billing | IBilling | *No description.* | +| controlPlaneEventSource | string | The source to use when listening for events coming from the SBT control plane. | +| eventMetadata | {[ key: string ]: string} | *No description.* | --- @@ -2213,43 +2555,51 @@ public readonly auth: IAuth; --- -##### `controlPlaneEventSource`Required +##### `applicationPlaneEventSource`Optional ```typescript -public readonly controlPlaneEventSource: string; +public readonly applicationPlaneEventSource: string; ``` - *Type:* string +The source to use for outgoing events that will be placed on the EventBus. + +This is used as the default if the OutgoingEventMetadata source field is not set. + --- -##### `offboardingDetailType`Required +##### `billing`Optional ```typescript -public readonly offboardingDetailType: string; +public readonly billing: IBilling; ``` -- *Type:* string +- *Type:* IBilling --- -##### `onboardingDetailType`Required +##### `controlPlaneEventSource`Optional ```typescript -public readonly onboardingDetailType: string; +public readonly controlPlaneEventSource: string; ``` - *Type:* string +The source to use when listening for events coming from the SBT control plane. + +This is used as the default if the IncomingEventMetadata source field is not set. + --- -##### `provisioningDetailType`Required +##### `eventMetadata`Optional ```typescript -public readonly provisioningDetailType: string; +public readonly eventMetadata: {[ key: string ]: string}; ``` -- *Type:* string +- *Type:* {[ key: string ]: string} --- @@ -2269,13 +2619,14 @@ const coreApplicationPlaneJobRunnerProps: CoreApplicationPlaneJobRunnerProps = { | **Name** | **Type** | **Description** | | --- | --- | --- | -| incomingEvent | IncomingEventMetadata | The IncomingEventMetadata to use when listening for the event that will trigger this CoreApplicationPlaneJobRunner. | +| incomingEvent | DetailType | The incoming event DetailType that triggers this job. | | name | string | The name of the CoreApplicationPlaneJobRunner. | -| outgoingEvent | OutgoingEventMetadata | The OutgoingEventMetadata to use when submitting a new event after this CoreApplicationPlaneJobRunner has executed. | +| outgoingEvent | DetailType | The outgoing event DetailType that is emitted upon job completion. | | permissions | aws-cdk-lib.aws_iam.PolicyDocument | The IAM permission document for the CoreApplicationPlaneJobRunner. | | script | string | The bash script to run as part of the CoreApplicationPlaneJobRunner. | -| exportedVariables | string[] | The environment variables to export into the outgoing event once the CoreApplicationPlaneJobRunner has finished. | -| importedVariables | string[] | The environment variables to import into the CoreApplicationPlaneJobRunner from event details field. | +| environmentJSONVariablesFromIncomingEvent | string[] | The environment variables to import into the CoreApplicationPlaneJobRunner from event details field. | +| environmentStringVariablesFromIncomingEvent | string[] | The environment variables to import into the CoreApplicationPlaneJobRunner from event details field. | +| environmentVariablesToOutgoingEvent | string[] | The environment variables to export into the outgoing event once the CoreApplicationPlaneJobRunner has finished. | | postScript | string | The bash script to run after the main script has completed. | | scriptEnvironmentVariables | {[ key: string ]: string} | The variables to pass into the codebuild CoreApplicationPlaneJobRunner. | @@ -2284,12 +2635,12 @@ const coreApplicationPlaneJobRunnerProps: CoreApplicationPlaneJobRunnerProps = { ##### `incomingEvent`Required ```typescript -public readonly incomingEvent: IncomingEventMetadata; +public readonly incomingEvent: DetailType; ``` -- *Type:* IncomingEventMetadata +- *Type:* DetailType -The IncomingEventMetadata to use when listening for the event that will trigger this CoreApplicationPlaneJobRunner. +The incoming event DetailType that triggers this job. --- @@ -2310,12 +2661,12 @@ Note that this value must be unique. ##### `outgoingEvent`Required ```typescript -public readonly outgoingEvent: OutgoingEventMetadata; +public readonly outgoingEvent: DetailType; ``` -- *Type:* OutgoingEventMetadata +- *Type:* DetailType -The OutgoingEventMetadata to use when submitting a new event after this CoreApplicationPlaneJobRunner has executed. +The outgoing event DetailType that is emitted upon job completion. --- @@ -2343,28 +2694,45 @@ The bash script to run as part of the CoreApplicationPlaneJobRunner. --- -##### `exportedVariables`Optional +##### `environmentJSONVariablesFromIncomingEvent`Optional ```typescript -public readonly exportedVariables: string[]; +public readonly environmentJSONVariablesFromIncomingEvent: string[]; ``` - *Type:* string[] -The environment variables to export into the outgoing event once the CoreApplicationPlaneJobRunner has finished. +The environment variables to import into the CoreApplicationPlaneJobRunner from event details field. + +This argument consists of the names of only JSON-formatted string type variables. +Ex. '{"test": 2}' --- -##### `importedVariables`Optional +##### `environmentStringVariablesFromIncomingEvent`Optional ```typescript -public readonly importedVariables: string[]; +public readonly environmentStringVariablesFromIncomingEvent: string[]; ``` - *Type:* string[] The environment variables to import into the CoreApplicationPlaneJobRunner from event details field. +This argument consists of the names of only string type variables. Ex. 'test' + +--- + +##### `environmentVariablesToOutgoingEvent`Optional + +```typescript +public readonly environmentVariablesToOutgoingEvent: string[]; +``` + +- *Type:* string[] + +The environment variables to export into the outgoing event once the CoreApplicationPlaneJobRunner has finished. + --- ##### `postScript`Optional @@ -2407,17 +2775,32 @@ const coreApplicationPlaneProps: CoreApplicationPlaneProps = { ... } | **Name** | **Type** | **Description** | | --- | --- | --- | -| applicationNamePlaneSource | string | The source to use for outgoing events that will be placed on the EventBus. | -| controlPlaneSource | string | The source to use when listening for events coming from the SBT control plane. | | eventBusArn | string | The arn belonging to the EventBus to listen for incoming messages. | +| applicationPlaneEventSource | string | The source to use for outgoing events that will be placed on the EventBus. | +| controlPlaneEventSource | string | The source to use when listening for events coming from the SBT control plane. | +| eventMetadata | {[ key: string ]: string} | *No description.* | | jobRunnerPropsList | CoreApplicationPlaneJobRunnerProps[] | The list of JobRunner definitions to create. | --- -##### `applicationNamePlaneSource`Required +##### `eventBusArn`Required + +```typescript +public readonly eventBusArn: string; +``` + +- *Type:* string + +The arn belonging to the EventBus to listen for incoming messages. + +This is also the EventBus on which the CoreApplicationPlane places outgoing messages on. + +--- + +##### `applicationPlaneEventSource`Optional ```typescript -public readonly applicationNamePlaneSource: string; +public readonly applicationPlaneEventSource: string; ``` - *Type:* string @@ -2428,10 +2811,10 @@ This is used as the default if the OutgoingEventMetadata source field is not set --- -##### `controlPlaneSource`Required +##### `controlPlaneEventSource`Optional ```typescript -public readonly controlPlaneSource: string; +public readonly controlPlaneEventSource: string; ``` - *Type:* string @@ -2442,17 +2825,13 @@ This is used as the default if the IncomingEventMetadata source field is not set --- -##### `eventBusArn`Required +##### `eventMetadata`Optional ```typescript -public readonly eventBusArn: string; +public readonly eventMetadata: {[ key: string ]: string}; ``` -- *Type:* string - -The arn belonging to the EventBus to listen for incoming messages. - -This is also the EventBus on which the CoreApplicationPlane places outgoing messages on. +- *Type:* {[ key: string ]: string} --- @@ -2485,6 +2864,9 @@ const eventManagerProps: EventManagerProps = { ... } | **Name** | **Type** | **Description** | | --- | --- | --- | | eventBus | aws-cdk-lib.aws_events.IEventBus | The event bus to register new rules with. | +| applicationPlaneEventSource | string | The source to use for outgoing events that will be placed on the EventBus. | +| controlPlaneEventSource | string | The source to use when listening for events coming from the SBT control plane. | +| eventMetadata | {[ key: string ]: string} | The EventMetadata to use to update the event defaults. | --- @@ -2500,19 +2882,126 @@ The event bus to register new rules with. --- -### IncomingEventMetadata +##### `applicationPlaneEventSource`Optional -Provides metadata for incoming events. +```typescript +public readonly applicationPlaneEventSource: string; +``` -#### Initializer +- *Type:* string -```typescript -import { IncomingEventMetadata } from '@cdklabs/sbt-aws' +The source to use for outgoing events that will be placed on the EventBus. -const incomingEventMetadata: IncomingEventMetadata = { ... } -``` +--- -#### Properties +##### `controlPlaneEventSource`Optional + +```typescript +public readonly controlPlaneEventSource: string; +``` + +- *Type:* string + +The source to use when listening for events coming from the SBT control plane. + +--- + +##### `eventMetadata`Optional + +```typescript +public readonly eventMetadata: {[ key: string ]: string}; +``` + +- *Type:* {[ key: string ]: string} + +The EventMetadata to use to update the event defaults. + +--- + +### FirehoseAggregatorProps + +Encapsulates the list of properties for a FirehoseAggregator construct. + +#### Initializer + +```typescript +import { FirehoseAggregatorProps } from '@cdklabs/sbt-aws' + +const firehoseAggregatorProps: FirehoseAggregatorProps = { ... } +``` + +#### Properties + +| **Name** | **Type** | **Description** | +| --- | --- | --- | +| aggregateKeyPath | string | The JMESPath to find the key value in the incoming data stream that will be aggregated. | +| aggregateValuePath | string | The JMESPath to find the numeric value of key in the incoming data stream that will be aggregated. | +| primaryKeyColumn | string | The name to use for the primary key column for the dynamoDB database. | +| primaryKeyPath | string | The JMESPath to find the primary key value in the incoming data stream. | + +--- + +##### `aggregateKeyPath`Required + +```typescript +public readonly aggregateKeyPath: string; +``` + +- *Type:* string + +The JMESPath to find the key value in the incoming data stream that will be aggregated. + +--- + +##### `aggregateValuePath`Required + +```typescript +public readonly aggregateValuePath: string; +``` + +- *Type:* string + +The JMESPath to find the numeric value of key in the incoming data stream that will be aggregated. + +--- + +##### `primaryKeyColumn`Required + +```typescript +public readonly primaryKeyColumn: string; +``` + +- *Type:* string + +The name to use for the primary key column for the dynamoDB database. + +--- + +##### `primaryKeyPath`Required + +```typescript +public readonly primaryKeyPath: string; +``` + +- *Type:* string + +The JMESPath to find the primary key value in the incoming data stream. + +--- + +### IncomingEventMetadata + +Provides metadata for incoming events. + +#### Initializer + +```typescript +import { IncomingEventMetadata } from '@cdklabs/sbt-aws' + +const incomingEventMetadata: IncomingEventMetadata = { ... } +``` + +#### Properties | **Name** | **Type** | **Description** | | --- | --- | --- | @@ -2540,7 +3029,7 @@ public readonly source: string[]; ``` - *Type:* string[] -- *Default:* CoreApplicationPlaneProps.controlPlaneSource +- *Default:* CoreApplicationPlaneProps.controlPlaneEventSource The list of sources to listen for in the incoming event. @@ -2586,7 +3075,7 @@ public readonly source: string; ``` - *Type:* string -- *Default:* CoreApplicationPlaneProps.applicationNamePlaneSource +- *Default:* CoreApplicationPlaneProps.applicationPlaneEventSource The source to set in the outgoing event. @@ -2606,42 +3095,19 @@ const servicesProps: ServicesProps = { ... } | **Name** | **Type** | **Description** | | --- | --- | --- | -| controlPlaneEventSource | string | *No description.* | -| eventBus | aws-cdk-lib.aws_events.EventBus | *No description.* | -| idpDetails | string | *No description.* | +| eventManager | EventManager | *No description.* | | lambdaLayer | aws-cdk-lib.aws_lambda.LayerVersion | *No description.* | -| onboardingDetailType | string | *No description.* | | tables | Tables | *No description.* | --- -##### `controlPlaneEventSource`Required +##### `eventManager`Required ```typescript -public readonly controlPlaneEventSource: string; +public readonly eventManager: EventManager; ``` -- *Type:* string - ---- - -##### `eventBus`Required - -```typescript -public readonly eventBus: EventBus; -``` - -- *Type:* aws-cdk-lib.aws_events.EventBus - ---- - -##### `idpDetails`Required - -```typescript -public readonly idpDetails: string; -``` - -- *Type:* string +- *Type:* EventManager --- @@ -2655,16 +3121,6 @@ public readonly lambdaLayer: LayerVersion; --- -##### `onboardingDetailType`Required - -```typescript -public readonly onboardingDetailType: string; -``` - -- *Type:* string - ---- - ##### `tables`Required ```typescript @@ -3098,3 +3554,271 @@ public readonly wellKnownEndpointUrl: string; --- +### IBilling + +- *Implemented By:* IBilling + +Encapsulates the list of properties for an IBilling construct. + + +#### Properties + +| **Name** | **Type** | **Description** | +| --- | --- | --- | +| createUserFunction | aws-cdk-lib.aws_lambda.IFunction | The function to trigger when creating a new billing user. | +| deleteUserFunction | aws-cdk-lib.aws_lambda.IFunction | The function to trigger when deleting a billing user. | +| ingestor | IDataIngestorAggregator | The IDataIngestorAggregator responsible for accepting and aggregating the raw billing data. | +| putUsageFunction | aws-cdk-lib.aws_lambda.IFunction | The function responsible for taking the aggregated data and pushing that to the billing provider. | +| webhookFunction | aws-cdk-lib.aws_lambda.IFunction | The function to trigger when a webhook request is received. | +| webhookPath | string | The path to the webhook resource. | + +--- + +##### `createUserFunction`Required + +```typescript +public readonly createUserFunction: IFunction; +``` + +- *Type:* aws-cdk-lib.aws_lambda.IFunction + +The function to trigger when creating a new billing user. + +--- + +##### `deleteUserFunction`Required + +```typescript +public readonly deleteUserFunction: IFunction; +``` + +- *Type:* aws-cdk-lib.aws_lambda.IFunction + +The function to trigger when deleting a billing user. + +--- + +##### `ingestor`Required + +```typescript +public readonly ingestor: IDataIngestorAggregator; +``` + +- *Type:* IDataIngestorAggregator + +The IDataIngestorAggregator responsible for accepting and aggregating the raw billing data. + +--- + +##### `putUsageFunction`Required + +```typescript +public readonly putUsageFunction: IFunction; +``` + +- *Type:* aws-cdk-lib.aws_lambda.IFunction + +The function responsible for taking the aggregated data and pushing that to the billing provider. + +--- + +##### `webhookFunction`Optional + +```typescript +public readonly webhookFunction: IFunction; +``` + +- *Type:* aws-cdk-lib.aws_lambda.IFunction + +The function to trigger when a webhook request is received. + +--- + +##### `webhookPath`Optional + +```typescript +public readonly webhookPath: string; +``` + +- *Type:* string + +The path to the webhook resource. + +--- + +### IDataIngestorAggregator + +- *Implemented By:* FirehoseAggregator, IDataIngestorAggregator + +Encapsulates the list of properties for a IDataIngestorAggregator. + + +#### Properties + +| **Name** | **Type** | **Description** | +| --- | --- | --- | +| dataAggregator | aws-cdk-lib.aws_lambda.IFunction | The function responsible for aggregating the raw data coming in via the dataIngestor. | +| dataIngestorName | string | The ingestor responsible for accepting and storing the incoming data. | +| dataRepository | aws-cdk-lib.aws_dynamodb.ITable | The table containing the aggregated data. | + +--- + +##### `dataAggregator`Required + +```typescript +public readonly dataAggregator: IFunction; +``` + +- *Type:* aws-cdk-lib.aws_lambda.IFunction + +The function responsible for aggregating the raw data coming in via the dataIngestor. + +--- + +##### `dataIngestorName`Required + +```typescript +public readonly dataIngestorName: string; +``` + +- *Type:* string + +The ingestor responsible for accepting and storing the incoming data. + +--- + +##### `dataRepository`Required + +```typescript +public readonly dataRepository: ITable; +``` + +- *Type:* aws-cdk-lib.aws_dynamodb.ITable + +The table containing the aggregated data. + +--- + +## Enums + +### DetailType + +Provides an easy way of accessing event DetailTypes. + +Note that the string represents the detailTypes used in +events sent across the EventBus. + +#### Members + +| **Name** | **Description** | +| --- | --- | +| ONBOARDING_REQUEST | *No description.* | +| ONBOARDING_SUCCESS | *No description.* | +| ONBOARDING_FAILURE | *No description.* | +| OFFBOARDING_REQUEST | *No description.* | +| OFFBOARDING_SUCCESS | *No description.* | +| OFFBOARDING_FAILURE | *No description.* | +| PROVISION_SUCCESS | *No description.* | +| PROVISION_FAILURE | *No description.* | +| DEPROVISION_SUCCESS | *No description.* | +| DEPROVISION_FAILURE | *No description.* | +| BILLING_SUCCESS | *No description.* | +| BILLING_FAILURE | *No description.* | +| ACTIVATE_REQUEST | *No description.* | +| ACTIVATE_SUCCESS | *No description.* | +| ACTIVATE_FAILURE | *No description.* | +| DEACTIVATE_REQUEST | *No description.* | +| DEACTIVATE_SUCCESS | *No description.* | +| DEACTIVATE_FAILURE | *No description.* | + +--- + +##### `ONBOARDING_REQUEST` + +--- + + +##### `ONBOARDING_SUCCESS` + +--- + + +##### `ONBOARDING_FAILURE` + +--- + + +##### `OFFBOARDING_REQUEST` + +--- + + +##### `OFFBOARDING_SUCCESS` + +--- + + +##### `OFFBOARDING_FAILURE` + +--- + + +##### `PROVISION_SUCCESS` + +--- + + +##### `PROVISION_FAILURE` + +--- + + +##### `DEPROVISION_SUCCESS` + +--- + + +##### `DEPROVISION_FAILURE` + +--- + + +##### `BILLING_SUCCESS` + +--- + + +##### `BILLING_FAILURE` + +--- + + +##### `ACTIVATE_REQUEST` + +--- + + +##### `ACTIVATE_SUCCESS` + +--- + + +##### `ACTIVATE_FAILURE` + +--- + + +##### `DEACTIVATE_REQUEST` + +--- + + +##### `DEACTIVATE_SUCCESS` + +--- + + +##### `DEACTIVATE_FAILURE` + +--- + diff --git a/package-lock.json b/package-lock.json index b5f9702..9abf03a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,15 +9,19 @@ "version": "0.0.0", "license": "Apache-2.0", "dependencies": { + "@aws-cdk/aws-kinesisfirehose-alpha": "^2.123.0-alpha.0", + "@aws-cdk/aws-kinesisfirehose-destinations-alpha": "^2.123.0-alpha.0", "@aws-cdk/aws-lambda-python-alpha": "^2.114.1-alpha.0", "cdk-nag": "^2.27.230" }, "devDependencies": { + "@aws-cdk/aws-kinesisfirehose-alpha": "2.123.0-alpha.0", "@types/jest": "^29.5.11", "@types/node": "^18", "@typescript-eslint/eslint-plugin": "^6", "@typescript-eslint/parser": "^6", - "aws-cdk-lib": "2.114.1", + "aws-cdk": "2.123.0", + "aws-cdk-lib": "2.123.0", "constructs": "10.0.5", "eslint": "^8", "eslint-config-prettier": "^9.1.0", @@ -43,7 +47,8 @@ "node": ">= 18.12.0 <= 20.x" }, "peerDependencies": { - "aws-cdk-lib": "^2.114.1", + "@aws-cdk/aws-kinesisfirehose-alpha": "^2.123.0-alpha.0", + "aws-cdk-lib": "^2.123.0", "constructs": "^10.0.5" } }, @@ -84,6 +89,31 @@ "resolved": "https://registry.npmjs.org/@aws-cdk/asset-node-proxy-agent-v6/-/asset-node-proxy-agent-v6-2.0.1.tgz", "integrity": "sha512-DDt4SLdLOwWCjGtltH4VCST7hpOI5DzieuhGZsBpZ+AgJdSI2GCjklCXm0GCTwJG/SolkL5dtQXyUKgg9luBDg==" }, + "node_modules/@aws-cdk/aws-kinesisfirehose-alpha": { + "version": "2.123.0-alpha.0", + "resolved": "https://registry.npmjs.org/@aws-cdk/aws-kinesisfirehose-alpha/-/aws-kinesisfirehose-alpha-2.123.0-alpha.0.tgz", + "integrity": "sha512-NbVzLjv4e6FRKg1BYypJNxX0pm0VZreFfW7Mh6uyqghiRUaQd4xcNoQz1lSL1XNUfiqshE9HGOs8gLB6PZC2yA==", + "engines": { + "node": ">= 14.15.0" + }, + "peerDependencies": { + "aws-cdk-lib": "^2.123.0", + "constructs": "^10.0.0" + } + }, + "node_modules/@aws-cdk/aws-kinesisfirehose-destinations-alpha": { + "version": "2.123.0-alpha.0", + "resolved": "https://registry.npmjs.org/@aws-cdk/aws-kinesisfirehose-destinations-alpha/-/aws-kinesisfirehose-destinations-alpha-2.123.0-alpha.0.tgz", + "integrity": "sha512-w8Wl2uMOD8DlI5q9PSxswXy0v0FRsfkP5WQ33uxoSf7blCyhXwuGVBbsyHrb+67DnOLZkSNT81uZYcxyyO5pLQ==", + "engines": { + "node": ">= 14.15.0" + }, + "peerDependencies": { + "@aws-cdk/aws-kinesisfirehose-alpha": "2.123.0-alpha.0", + "aws-cdk-lib": "^2.123.0", + "constructs": "^10.0.0" + } + }, "node_modules/@aws-cdk/aws-lambda-python-alpha": { "version": "2.114.1-alpha.0", "resolved": "https://registry.npmjs.org/@aws-cdk/aws-lambda-python-alpha/-/aws-lambda-python-alpha-2.114.1-alpha.0.tgz", @@ -2085,10 +2115,25 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/aws-cdk": { + "version": "2.123.0", + "resolved": "https://registry.npmjs.org/aws-cdk/-/aws-cdk-2.123.0.tgz", + "integrity": "sha512-JvGNN1FobSaGwirJJQZ1oIkaHFfQoLbRyuxzFNQSs2wlVltwFb1VdR7FNxh0sVzugM2RsYQu8xQPUa53ZnDlyg==", + "dev": true, + "bin": { + "cdk": "bin/cdk" + }, + "engines": { + "node": ">= 14.15.0" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, "node_modules/aws-cdk-lib": { - "version": "2.114.1", - "resolved": "https://registry.npmjs.org/aws-cdk-lib/-/aws-cdk-lib-2.114.1.tgz", - "integrity": "sha512-pJy+Sa3+s6K9I0CXYGU8J5jumw9uQEbl8zPK8EMA+A6hP9qb1JN+a8ohyw6a1O1cb4D5S6gwH+hE7Fq7hGPY3A==", + "version": "2.123.0", + "resolved": "https://registry.npmjs.org/aws-cdk-lib/-/aws-cdk-lib-2.123.0.tgz", + "integrity": "sha512-KSfX1ex52N/v25hjOlec1D9iCBLbGpegTR8rB4kYVqdQCdCWbKAwZjo6f38JZqkDlEJn+g249p8xiniq/gDniQ==", "bundleDependencies": [ "@balena/dockerignore", "case", @@ -2102,12 +2147,12 @@ "yaml" ], "dependencies": { - "@aws-cdk/asset-awscli-v1": "^2.2.201", + "@aws-cdk/asset-awscli-v1": "^2.2.202", "@aws-cdk/asset-kubectl-v20": "^2.1.2", "@aws-cdk/asset-node-proxy-agent-v6": "^2.0.1", "@balena/dockerignore": "^1.0.2", "case": "1.6.3", - "fs-extra": "^11.1.1", + "fs-extra": "^11.2.0", "ignore": "^5.3.0", "jsonschema": "^1.4.1", "minimatch": "^3.1.2", @@ -2227,7 +2272,7 @@ "license": "MIT" }, "node_modules/aws-cdk-lib/node_modules/fs-extra": { - "version": "11.1.1", + "version": "11.2.0", "inBundle": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 74683d0..e620ab5 100644 --- a/package.json +++ b/package.json @@ -35,11 +35,13 @@ "organization": true }, "devDependencies": { + "@aws-cdk/aws-kinesisfirehose-alpha": "2.123.0-alpha.0", "@types/jest": "^29.5.11", "@types/node": "^18", "@typescript-eslint/eslint-plugin": "^6", "@typescript-eslint/parser": "^6", - "aws-cdk-lib": "2.114.1", + "aws-cdk": "2.123.0", + "aws-cdk-lib": "2.123.0", "constructs": "10.0.5", "eslint": "^8", "eslint-config-prettier": "^9.1.0", @@ -62,10 +64,13 @@ "typescript": "^5.1.6" }, "peerDependencies": { - "aws-cdk-lib": "^2.114.1", + "@aws-cdk/aws-kinesisfirehose-alpha": "^2.123.0-alpha.0", + "aws-cdk-lib": "^2.123.0", "constructs": "^10.0.5" }, "dependencies": { + "@aws-cdk/aws-kinesisfirehose-alpha": "^2.123.0-alpha.0", + "@aws-cdk/aws-kinesisfirehose-destinations-alpha": "^2.123.0-alpha.0", "@aws-cdk/aws-lambda-python-alpha": "^2.114.1-alpha.0", "cdk-nag": "^2.27.230" }, diff --git a/resources/functions/data-aggregator/index.py b/resources/functions/data-aggregator/index.py new file mode 100644 index 0000000..8c34770 --- /dev/null +++ b/resources/functions/data-aggregator/index.py @@ -0,0 +1,61 @@ +from aws_lambda_powertools.utilities.data_classes import event_source, S3Event +from urllib.parse import unquote_plus +import boto3 # relying on lambda runtime to provide boto3 https://docs.aws.amazon.com/lambda/latest/dg/lambda-runtimes.html +import os +from aws_lambda_powertools import Logger +from aws_lambda_powertools import Tracer +import json +import jmespath + +tracer = Tracer() +logger = Logger(service=os.environ['SERVICE_NAME']) +dynamodb = boto3.resource("dynamodb") +s3_client = boto3.client('s3') +data_table = dynamodb.Table(os.environ['DATA_TABLE']) +primary_key_column = os.environ['PRIMARY_KEY_COLUMN'] +primary_key_path = os.environ['PRIMARY_KEY_PATH'] +aggregate_key_path = os.environ['AGGREGATE_KEY_PATH'] +aggregate_value_path = os.environ['AGGREGATE_VALUE_PATH'] + + +@event_source(data_class=S3Event) +def handler(event: S3Event, context): + bucket_name = event.bucket_name + logger.info(event) + + # Multiple records can be delivered in a single event + for record in event.records: + object_key = unquote_plus(record.s3.get_object.key) + obj = s3_client.get_object(Bucket=event.bucket_name, Key=object_key) + lines = obj['Body'].read().decode('utf-8').splitlines() + + for line in lines: + logger.info(line) + data = json.loads(line) + logger.info(data) + + primary_key = jmespath.search(primary_key_path, data) + aggregate_key = jmespath.search(aggregate_key_path, data) + aggregate_value = jmespath.search(aggregate_value_path, data) + + logger.info(f"primary_key: {primary_key}") + logger.info(f"aggregate_key: {aggregate_key}") + logger.info(f"aggregate_value: {aggregate_value}") + if primary_key is None or aggregate_key is None or aggregate_value is None: + logger.info(f"Skipping line {line}. Required data not found.") + continue + + # perform the following as an atomic operation + # i.e., read and update in one-go + # if we break it up into 2 operations, it's possible + # that after we read, another process updates the aggregate_value + # so then adding to the value that we just read would result + # in the wrong sum. + response = data_table.update_item( + Key={primary_key_column: primary_key}, + UpdateExpression=f"SET {aggregate_key} = if_not_exists({aggregate_key}, :default_val) + :val", + ExpressionAttributeValues={':val': int(aggregate_value), ':default_val': 0} + ) + logger.info(f"update response: {response}") + + logger.info(f"{bucket_name}/{object_key}") diff --git a/resources/functions/models/control_plane_event_types.py b/resources/functions/models/control_plane_event_types.py deleted file mode 100644 index 9562716..0000000 --- a/resources/functions/models/control_plane_event_types.py +++ /dev/null @@ -1,14 +0,0 @@ -# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -# SPDX-License-Identifier: Apache-2.0 - -from enum import Enum - - -class ControlPlaneEventTypes(Enum): - ONBOARDING = 'Onboarding' - OFFBOARDING = 'Offboarding' - ACTIVATE = 'Activate' - DEACTIVATE = 'Deactivate' - - def __str__(self): - return str(self.value) diff --git a/resources/functions/tenant_management.py b/resources/functions/tenant-management/tenant_management.py similarity index 62% rename from resources/functions/tenant_management.py rename to resources/functions/tenant-management/tenant_management.py index 155ec42..a7138ef 100644 --- a/resources/functions/tenant_management.py +++ b/resources/functions/tenant-management/tenant_management.py @@ -7,11 +7,16 @@ import uuid import boto3 +import botocore from aws_lambda_powertools import Logger, Tracer from aws_lambda_powertools.event_handler import (APIGatewayRestResolver, CORSConfig) from aws_lambda_powertools.logging import correlation_paths -from models.control_plane_event_types import ControlPlaneEventTypes +from aws_lambda_powertools.event_handler.exceptions import ( + BadRequestError, + InternalServerError, + NotFoundError, +) tracer = Tracer() logger = Logger() @@ -22,6 +27,10 @@ event_bus = boto3.client('events') eventbus_name = os.environ['EVENTBUS_NAME'] event_source = os.environ['EVENT_SOURCE'] +onboarding_detail_type = os.environ['ONBOARDING_DETAIL_TYPE'] +offboarding_detail_type = os.environ['OFFBOARDING_DETAIL_TYPE'] +activate_detail_type = os.environ['ACTIVATE_DETAIL_TYPE'] +deactivate_detail_type = os.environ['DEACTIVATE_DETAIL_TYPE'] dynamodb = boto3.resource('dynamodb') tenant_details_table = dynamodb.Table(os.environ['TENANT_DETAILS_TABLE']) @@ -43,36 +52,44 @@ def create_tenant(): response = tenant_details_table.put_item(Item=input_item) __create_control_plane_event( - json.dumps(input_details), ControlPlaneEventTypes.ONBOARDING.value) + json.dumps(input_details), onboarding_detail_type) except Exception as e: raise Exception("Error creating a new tenant", e) else: - return "New tenant created", HTTPStatus.OK + return {'data': input_item}, HTTPStatus.CREATED @app.get("/tenants") @tracer.capture_method def get_tenants(): logger.info("Request received to get all tenants") + tenants = None try: response = tenant_details_table.scan() - except Exception as e: - raise Exception('Error getting all tenants', e) + tenants = response['Items'] + except botocore.exceptions.ClientError as error: + logger.error(error) + raise InternalServerError("Unknown error during processing!") else: - return response['Items'], HTTPStatus.OK + return {'data': tenants}, HTTPStatus.OK @app.get("/tenants/") @tracer.capture_method def get_tenant(tenantId): - logger.info("Request received to get a tenant") + logger.info(f"Request received to get a tenant: {tenantId}") + tenant = None try: response = tenant_details_table.get_item(Key={'tenantId': tenantId}) - except Exception as e: - raise Exception('Error getting tenant', e) + tenant = response.get('Item') + if not tenant: + raise NotFoundError(f"Tenant not found for id {tenantId}") + except botocore.exceptions.ClientError as error: + logger.error(error) + raise InternalServerError("Unknown error during processing!") else: - return response['Item'], HTTPStatus.OK + return {'data': tenant}, HTTPStatus.OK @app.put("/tenants/") @@ -80,29 +97,31 @@ def get_tenant(tenantId): def update_tenant(tenantId): logger.info("Request received to update a tenant") input_details = app.current_event.json_body - + updated_tenant = None try: - __update_tenant(tenantId, input_details) - except Exception as e: - raise Exception("Error updating a tenant", e) + response = __update_tenant(tenantId, input_details) + updated_tenant = response['Attributes'] + except botocore.exceptions.ClientError as error: + logger.error(error) + raise InternalServerError("Unknown error during processing!") else: - return "Tenant updated", HTTPStatus.OK + return {'data': updated_tenant}, HTTPStatus.OK @app.delete("/tenants/") @tracer.capture_method def delete_tenant(tenantId): logger.info("Request received to delete a tenant") - input_details = {**app.current_event.json_body, 'tenantStatus': 'Deleting'} try: - __update_tenant(tenantId, input_details) + response = __update_tenant(tenantId, {'tenantStatus': 'Deleting'}) __create_control_plane_event( - json.dumps(input_details), ControlPlaneEventTypes.OFFBOARDING.value) - except Exception as e: - raise Exception("Error deleting a tenant", e) + json.dumps(response['Attributes']), offboarding_detail_type) + except botocore.exceptions.ClientError as error: + logger.error(error) + raise InternalServerError("Unknown error during processing!") else: - return 'Successsfuly sent offboarding message to application plane', HTTPStatus.OK + return {"message": "Successfully sent offboarding message to application plane"}, HTTPStatus.OK def __update_tenant(tenantId, tenant): @@ -120,13 +139,13 @@ def __update_tenant(tenantId, tenant): # remove the last comma update_expression.pop() - response_update = tenant_details_table.update_item( + return tenant_details_table.update_item( Key={ 'tenantId': tenantId, }, UpdateExpression=''.join(update_expression), ExpressionAttributeValues=expression_attribute_values, - ReturnValues="UPDATED_NEW" + ReturnValues="ALL_NEW" ) @@ -148,12 +167,13 @@ def deactivate_tenant(tenantId): ) __create_control_plane_event( - json.dumps({"tenantId": tenantId}), ControlPlaneEventTypes.DEACTIVATE.value) - except Exception as e: - raise Exception("Error while deactivating a tenant", e) + json.dumps({"tenantId": tenantId}), deactivate_detail_type) + except botocore.exceptions.ClientError as error: + logger.error(error) + raise InternalServerError("Unknown error during processing!") else: - return "Tenant deactivated", HTTPStatus.OK + return {"message": "Tenant deactivated"}, HTTPStatus.OK @app.put("/tenants//activate") @@ -174,22 +194,23 @@ def activate_tenant(tenantId): ) __create_control_plane_event(json.dumps( - response['Attributes']), ControlPlaneEventTypes.ACTIVATE.value) - except Exception as e: - raise Exception("Error while activating a tenant", e) + response['Attributes']), activate_detail_type) + except botocore.exceptions.ClientError as error: + logger.error(error) + raise InternalServerError("Unknown error during processing!") else: - return "Tenant activated", HTTPStatus.OK + return {"message": "Tenant activated"}, HTTPStatus.OK -def __create_control_plane_event(eventDetails, eventType): +def __create_control_plane_event(event_details, detail_type): response = event_bus.put_events( Entries=[ { 'EventBusName': eventbus_name, 'Source': event_source, - 'DetailType': eventType, - 'Detail': eventDetails, + 'DetailType': detail_type, + 'Detail': event_details, } ] ) diff --git a/resources/layers/models/control_plane_event_types.py b/resources/layers/models/control_plane_event_types.py deleted file mode 100644 index 9680c59..0000000 --- a/resources/layers/models/control_plane_event_types.py +++ /dev/null @@ -1,14 +0,0 @@ -# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -# SPDX-License-Identifier: Apache-2.0 - -"""Module providingFunction printing python version.""" -from enum import Enum - - -class ControlPlaneEventTypes(Enum): - """Enum of possible event types that should be handled by the app plane""" - ONBOARDING = 'Onboarding' - OFFBOARDING = 'Offboarding' - - def __str__(self): - return str(self.value) diff --git a/src/control-plane/billing/billing-interface.ts b/src/control-plane/billing/billing-interface.ts new file mode 100644 index 0000000..bda4b03 --- /dev/null +++ b/src/control-plane/billing/billing-interface.ts @@ -0,0 +1,42 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { IFunction } from 'aws-cdk-lib/aws-lambda'; +import { IDataIngestorAggregator } from '../ingestor-aggregator/ingestor-aggregator-interface'; + +/** + * Encapsulates the list of properties for an IBilling construct. + */ +export interface IBilling { + /** + * The function to trigger when creating a new billing user. + */ + createUserFunction: IFunction; + + /** + * The function to trigger when deleting a billing user. + */ + deleteUserFunction: IFunction; + + /** + * The IDataIngestorAggregator responsible for accepting and aggregating + * the raw billing data. + */ + ingestor: IDataIngestorAggregator; + + /** + * The function responsible for taking the aggregated data and pushing + * that to the billing provider. + */ + putUsageFunction: IFunction; + + /** + * The function to trigger when a webhook request is received. + */ + webhookFunction?: IFunction; + + /** + * The path to the webhook resource. + */ + webhookPath?: string; +} diff --git a/src/control-plane/billing/billing-provider.ts b/src/control-plane/billing/billing-provider.ts new file mode 100644 index 0000000..48f08b0 --- /dev/null +++ b/src/control-plane/billing/billing-provider.ts @@ -0,0 +1,87 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import * as cdk from 'aws-cdk-lib'; +import { IResource, Resource } from 'aws-cdk-lib/aws-apigateway'; +import * as aws_events from 'aws-cdk-lib/aws-events'; +import * as event_targets from 'aws-cdk-lib/aws-events-targets'; +import { NagSuppressions } from 'cdk-nag'; +import { Construct } from 'constructs'; +import { IBilling } from './billing-interface'; +import { EventManager, DetailType } from '../../utils'; + +/** + * Encapsulates the list of properties for a BillingProvider. + */ +export interface BillingProviderProps { + /** + * An implementation of the IBilling interface. + */ + readonly billing: IBilling; + + /** + * An EventManager object to help coordinate events. + */ + readonly eventManager: EventManager; + + /** + * An API Gateway Resource for the BillingProvider to use + * when setting up API endpoints. + */ + readonly controlPlaneAPIBillingResource: Resource; +} + +export class BillingProvider extends Construct { + /** + * The API Gateway resource containing the billing webhook resource. + * Only set when the IBilling webhookFunction is defined. + */ + public readonly controlPlaneAPIBillingWebhookResource?: IResource; + constructor(scope: Construct, id: string, props: BillingProviderProps) { + super(scope, id); + + props.eventManager.addTargetToEvent( + DetailType.PROVISION_SUCCESS, + new event_targets.LambdaFunction(props.billing.createUserFunction) + ); + + props.eventManager.addTargetToEvent( + DetailType.DEPROVISION_SUCCESS, + new event_targets.LambdaFunction(props.billing.deleteUserFunction) + ); + + new aws_events.Rule(this, 'BillingPutUsageRule', { + schedule: aws_events.Schedule.rate(cdk.Duration.hours(24)), + targets: [new event_targets.LambdaFunction(props.billing.putUsageFunction)], + }); + + if (props.billing.webhookFunction && props.billing.webhookPath) { + this.controlPlaneAPIBillingWebhookResource = props.controlPlaneAPIBillingResource.addResource( + props.billing.webhookPath + ); + + this.controlPlaneAPIBillingWebhookResource.addMethod( + 'POST', + new cdk.aws_apigateway.LambdaIntegration(props.billing.webhookFunction) + ); + + NagSuppressions.addResourceSuppressionsByPath( + cdk.Stack.of(this), + [ + `${this.controlPlaneAPIBillingWebhookResource}/OPTIONS/Resource`, + `${this.controlPlaneAPIBillingWebhookResource}/POST/Resource`, + ], + [ + { + id: 'AwsSolutions-APIG4', + reason: 'Authorization not needed for webhook function or OPTIONS method.', + }, + { + id: 'AwsSolutions-COG4', + reason: 'These methods do not require a cognito authorizer.', + }, + ] + ); + } + } +} diff --git a/src/control-plane/billing/index.ts b/src/control-plane/billing/index.ts new file mode 100644 index 0000000..66c3a8c --- /dev/null +++ b/src/control-plane/billing/index.ts @@ -0,0 +1,5 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +export * from './billing-provider'; +export * from './billing-interface'; diff --git a/src/control-plane/control-plane-api.ts b/src/control-plane/control-plane-api.ts index aece4ea..5654e15 100644 --- a/src/control-plane/control-plane-api.ts +++ b/src/control-plane/control-plane-api.ts @@ -21,11 +21,12 @@ export interface ControlPlaneAPIProps { export class ControlPlaneAPI extends Construct { apiUrl: any; + public readonly billingResource: apigateway.Resource; public readonly tenantUpdateServiceTarget: targets.ApiGateway; constructor(scope: Construct, id: string, props: ControlPlaneAPIProps) { super(scope, id); - const controlPlaneAPILogGroup = new LogGroup(this, 'PrdLogs', { + const controlPlaneAPILogGroup = new LogGroup(this, 'controlPlaneAPILogGroup', { retention: RetentionDays.ONE_WEEK, }); const controlPlaneAPI = new apigateway.RestApi(this, 'controlPlaneAPI', { @@ -109,6 +110,21 @@ export class ControlPlaneAPI extends Construct { ] ); + this.billingResource = controlPlaneAPI.root.addResource('billing'); + NagSuppressions.addResourceSuppressionsByPath( + cdk.Stack.of(this), + `${this.billingResource}/OPTIONS/Resource`, + [ + { + id: 'AwsSolutions-APIG4', + reason: 'Authorization not needed for OPTION method.', + }, + { + id: 'AwsSolutions-COG4', + reason: 'Cognito Authorization not needed for OPTION method.', + }, + ] + ); const tenants = controlPlaneAPI.root.addResource('tenants'); tenants.addMethod( 'POST', @@ -330,12 +346,14 @@ export class ControlPlaneAPI extends Construct { ); const tenantConfig = controlPlaneAPI.root.addResource('tenant-config'); + // todo: move to tenantConfig module tenantConfig.addMethod( 'GET', new apigateway.LambdaIntegration(props.tenantConfigServiceLambda) ); const tenantConfigNameResource = tenantConfig.addResource('{tenantName}'); + // todo: move to tenantConfig module tenantConfigNameResource.addMethod( 'GET', new apigateway.LambdaIntegration(props.tenantConfigServiceLambda) diff --git a/src/control-plane/control-plane.ts b/src/control-plane/control-plane.ts index 99166c4..715db2b 100644 --- a/src/control-plane/control-plane.ts +++ b/src/control-plane/control-plane.ts @@ -6,6 +6,7 @@ import { EventBus } from 'aws-cdk-lib/aws-events'; import { NagSuppressions } from 'cdk-nag'; import { Construct } from 'constructs'; import { IAuth } from './auth'; +import { IBilling, BillingProvider } from './billing'; import { ControlPlaneAPI } from './control-plane-api'; import { LambdaLayers } from './lambda-layers'; import { Messaging } from './messaging'; @@ -13,23 +14,30 @@ import { Services } from './services'; import { Tables } from './tables'; import { TenantConfigService } from './tenant-config/tenant-config-service'; import { DestroyPolicySetter } from '../cdk-aspect/destroy-policy-setter'; -import { EventManager, setTemplateDesc } from '../utils'; +import { EventManager, setTemplateDesc, EventMetadata, DetailType } from '../utils'; export interface ControlPlaneProps { - readonly applicationPlaneEventSource: string; - readonly provisioningDetailType: string; - readonly controlPlaneEventSource: string; - readonly onboardingDetailType: string; - readonly offboardingDetailType: string; readonly auth: IAuth; + readonly billing?: IBilling; + readonly eventMetadata?: EventMetadata; + /** + * The source to use when listening for events coming from the SBT control plane. + * This is used as the default if the IncomingEventMetadata source field is not set. + */ + readonly controlPlaneEventSource?: string; + + /** + * The source to use for outgoing events that will be placed on the EventBus. + * This is used as the default if the OutgoingEventMetadata source field is not set. + */ + readonly applicationPlaneEventSource?: string; } export class ControlPlane extends Construct { readonly eventBusArn: string; - readonly controlPlaneSource: string; - readonly onboardingDetailType: string; - readonly offboardingDetailType: string; + readonly eventManager: EventManager; readonly controlPlaneAPIGatewayUrl: string; + readonly tables: Tables; constructor(scope: Construct, id: string, props: ControlPlaneProps) { super(scope, id); @@ -40,22 +48,27 @@ export class ControlPlane extends Construct { const messaging = new Messaging(this, 'messaging-stack'); const lambdaLayers = new LambdaLayers(this, 'controlplane-lambda-layers'); - const tables = new Tables(this, 'tables-stack'); + this.tables = new Tables(this, 'tables-stack'); + + const eventBus = EventBus.fromEventBusArn(this, 'eventBus', messaging.eventBus.eventBusArn); + this.eventManager = new EventManager(this, 'EventManager', { + eventBus: eventBus, + eventMetadata: props.eventMetadata, + applicationPlaneEventSource: props.applicationPlaneEventSource, + controlPlaneEventSource: props.controlPlaneEventSource, + }); const services = new Services(this, 'services-stack', { - eventBus: messaging.eventBus, - idpDetails: props.auth.controlPlaneIdpDetails, lambdaLayer: lambdaLayers.controlPlaneLambdaLayer, - tables: tables, - onboardingDetailType: props.onboardingDetailType, - controlPlaneEventSource: props.controlPlaneEventSource, + tables: this.tables, + eventManager: this.eventManager, }); const tenantConfigService = new TenantConfigService(this, 'auth-info-service-stack', { - tenantDetails: tables.tenantDetails, - tenantDetailsTenantNameColumn: tables.tenantNameColumn, - tenantConfigIndexName: tables.tenantConfigIndexName, - tenantDetailsTenantConfigColumn: tables.tenantConfigColumn, + tenantDetails: this.tables.tenantDetails, + tenantDetailsTenantNameColumn: this.tables.tenantNameColumn, + tenantConfigIndexName: this.tables.tenantConfigIndexName, + tenantDetailsTenantConfigColumn: this.tables.tenantConfigColumn, }); const controlPlaneAPI = new ControlPlaneAPI(this, 'controlplane-api-stack', { @@ -65,26 +78,15 @@ export class ControlPlane extends Construct { }); this.eventBusArn = messaging.eventBus.eventBusArn; - this.controlPlaneSource = props.controlPlaneEventSource; - this.onboardingDetailType = props.onboardingDetailType; - this.offboardingDetailType = props.offboardingDetailType; this.controlPlaneAPIGatewayUrl = controlPlaneAPI.apiUrl; - const eventBus = EventBus.fromEventBusArn(this, 'eventBus', messaging.eventBus.eventBusArn); - const eventManager = new EventManager(this, 'EventManager', { - eventBus: eventBus, - }); - - eventManager.addRuleWithTarget( - 'ProvisioningServiceRule', - [props.onboardingDetailType], - [props.applicationPlaneEventSource], + this.eventManager.addTargetToEvent( + DetailType.PROVISION_SUCCESS, controlPlaneAPI.tenantUpdateServiceTarget ); - eventManager.addRuleWithTarget( - 'DeprovisioningServiceRule', - [props.offboardingDetailType], - [props.applicationPlaneEventSource], + + this.eventManager.addTargetToEvent( + DetailType.DEPROVISION_SUCCESS, controlPlaneAPI.tenantUpdateServiceTarget ); @@ -93,6 +95,28 @@ export class ControlPlane extends Construct { key: 'controlPlaneAPIGatewayUrl', }); + new cdk.CfnOutput(this, 'eventBridgeArn', { + value: this.eventManager.eventBus.eventBusArn, + key: 'eventBridgeArn', + }); + + if (props.billing) { + const billingTemplate = new BillingProvider(this, 'Billing', { + billing: props.billing, + eventManager: this.eventManager, + controlPlaneAPIBillingResource: controlPlaneAPI.billingResource, + }); + + if (billingTemplate.controlPlaneAPIBillingWebhookResource) { + new cdk.CfnOutput(this, 'billingWebhookURL', { + value: `${ + controlPlaneAPI.apiUrl + }${billingTemplate.controlPlaneAPIBillingWebhookResource.path.substring(1)}`, + key: 'billingWebhookURL', + }); + } + } + // defined suppression here to suppress EventsRole Default policy // which gets updated in EventManager construct, but is part of ControlPlane API NagSuppressions.addResourceSuppressions( @@ -108,33 +132,5 @@ export class ControlPlane extends Construct { ], true // applyToChildren = true, so that it applies to the APIGW role created by cdk in the controlPlaneAPI construct ); - - // defined here as these log retention resources are not - // created as part of a lower-level construct - NagSuppressions.addResourceSuppressionsByPath( - cdk.Stack.of(this), - [ - `${ - cdk.Stack.of(this).stackName - }/LogRetentionaae0aa3c5b4d4f87b02d85b201efdd8a/ServiceRole/Resource`, - `${ - cdk.Stack.of(this).stackName - }/LogRetentionaae0aa3c5b4d4f87b02d85b201efdd8a/ServiceRole/DefaultPolicy/Resource`, - ], - [ - { - id: 'AwsSolutions-IAM4', - reason: 'Suppress error from resource created for setting log retention.', - appliesTo: [ - 'Policy::arn::iam::aws:policy/service-role/AWSLambdaBasicExecutionRole', - ], - }, - { - id: 'AwsSolutions-IAM5', - reason: 'Suppress error from resource created for setting log retention.', - appliesTo: ['Resource::*'], - }, - ] - ); } } diff --git a/src/control-plane/index.ts b/src/control-plane/index.ts index 0c4cf9d..f30213f 100644 --- a/src/control-plane/index.ts +++ b/src/control-plane/index.ts @@ -9,4 +9,6 @@ export * from './messaging'; export * from './models/tenant'; export * from './services'; export * from './tables'; +export * from './billing/index'; +export * from './ingestor-aggregator/index'; export * from './tenant-config/tenant-config-service'; diff --git a/src/control-plane/ingestor-aggregator/firehose-ingestor-aggregator.ts b/src/control-plane/ingestor-aggregator/firehose-ingestor-aggregator.ts new file mode 100644 index 0000000..10af423 --- /dev/null +++ b/src/control-plane/ingestor-aggregator/firehose-ingestor-aggregator.ts @@ -0,0 +1,198 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import * as path from 'path'; +import * as firehose from '@aws-cdk/aws-kinesisfirehose-alpha'; +import * as destinations from '@aws-cdk/aws-kinesisfirehose-destinations-alpha'; +import * as lambda_python from '@aws-cdk/aws-lambda-python-alpha'; +import * as cdk from 'aws-cdk-lib'; +import * as dynamodb from 'aws-cdk-lib/aws-dynamodb'; +import * as lambda from 'aws-cdk-lib/aws-lambda'; +import * as s3 from 'aws-cdk-lib/aws-s3'; +import * as s3n from 'aws-cdk-lib/aws-s3-notifications'; +import { NagSuppressions } from 'cdk-nag'; +import { Construct } from 'constructs'; +import { IDataIngestorAggregator } from './ingestor-aggregator-interface'; + +/** + * Encapsulates the list of properties for a FirehoseAggregator construct. + */ +export interface FirehoseAggregatorProps { + /** + * The name to use for the primary key column for the dynamoDB database. + */ + readonly primaryKeyColumn: string; + + /** + * The JMESPath to find the primary key value in the incoming data stream. + */ + readonly primaryKeyPath: string; + + /** + * The JMESPath to find the key value in the incoming data stream that will be aggregated. + */ + readonly aggregateKeyPath: string; + + /** + * The JMESPath to find the numeric value of key in the incoming data stream that will be aggregated. + */ + readonly aggregateValuePath: string; +} + +/** + * Creates a Kinesis Firehose to accept high-volume data, which it then routes to an s3 bucket. + * The s3 bucket triggers a lambda which processes the data and stores it in a DynamoDB table + * containing the aggregated data. + */ +export class FirehoseAggregator extends Construct implements IDataIngestorAggregator { + /** + * The DynamoDB table containing the aggregated data. + */ + public readonly dataRepository: dynamodb.ITable; + + /** + * The Python Lambda function responsible for aggregating the raw data coming in + * via the dataIngestor. + */ + public readonly dataAggregator: lambda.IFunction; + + /** + * The Firehose DeliveryStream ingestor responsible for accepting the incoming data. + */ + public readonly dataIngestor: firehose.DeliveryStream; + + /** + * The name of the dataIngestor. This is used for visibility. + */ + public readonly dataIngestorName: string; + + constructor(scope: Construct, id: string, props: FirehoseAggregatorProps) { + super(scope, id); + + const serviceName = 'FirehoseAggregator'; + + const firehoseDestinationBucket = new s3.Bucket(this, 'FirehoseDestinationBucket', { + enforceSSL: true, + }); + + NagSuppressions.addResourceSuppressions( + firehoseDestinationBucket, + [ + { + id: 'AwsSolutions-S1', + reason: 'Server access logs not required.', + }, + ], + true // applyToChildren = true, so that it applies to policies created for the role. + ); + + this.dataIngestor = new firehose.DeliveryStream(this, 'Firehose', { + encryption: firehose.StreamEncryption.AWS_OWNED, + destinations: [new destinations.S3Bucket(firehoseDestinationBucket)], + }); + + this.dataIngestorName = this.dataIngestor.deliveryStreamName; + + NagSuppressions.addResourceSuppressions( + this.dataIngestor, + [ + { + id: 'AwsSolutions-IAM5', + reason: 'S3 object resource name(s) not known beforehand.', + }, + ], + true // applyToChildren = true, so that it applies to policies created for the role. + ); + + this.dataRepository = new dynamodb.Table(this, 'Data', { + partitionKey: { name: props.primaryKeyColumn, type: dynamodb.AttributeType.STRING }, + pointInTimeRecovery: true, + }); + + // https://docs.powertools.aws.dev/lambda/python/2.31.0/#lambda-layer + const lambdaPowerToolsLayerARN = `arn:aws:lambda:${ + cdk.Stack.of(this).region + }:017000801446:layer:AWSLambdaPowertoolsPythonV2:59`; + + this.dataAggregator = new lambda_python.PythonFunction(this, 'DataAggregatorLambda', { + entry: path.join(__dirname, '../../../resources/functions/data-aggregator'), + runtime: lambda.Runtime.PYTHON_3_12, + index: 'index.py', + handler: 'handler', + tracing: lambda.Tracing.ACTIVE, + timeout: cdk.Duration.seconds(30), + environment: { + SERVICE_NAME: serviceName, + DATA_TABLE: this.dataRepository.tableName, + PRIMARY_KEY_COLUMN: props.primaryKeyColumn, + PRIMARY_KEY_PATH: props.primaryKeyPath, + AGGREGATE_KEY_PATH: props.aggregateKeyPath, + AGGREGATE_VALUE_PATH: props.aggregateValuePath, + }, + logGroup: new cdk.aws_logs.LogGroup(this, 'DataAggregatorLambdaLogGroup', { + retention: cdk.aws_logs.RetentionDays.FIVE_DAYS, + }), + layers: [ + lambda.LayerVersion.fromLayerVersionArn(this, 'LambdaPowerTools', lambdaPowerToolsLayerARN), + ], + }); + + NagSuppressions.addResourceSuppressions( + [this.dataAggregator.role!], + [ + { + id: 'AwsSolutions-IAM4', + reason: 'Suppress usage of AWSLambdaBasicExecutionRole.', + appliesTo: [ + 'Policy::arn::iam::aws:policy/service-role/AWSLambdaBasicExecutionRole', + ], + }, + { + id: 'AwsSolutions-IAM5', + reason: + 'This is Resource::* being used to output logs and x-ray traces and nothing else.', + appliesTo: [ + 'Resource::*', + 'Action::s3:GetBucket*', + 'Action::s3:GetObject*', + 'Action::s3:List*', + `Resource::<${cdk.Stack.of(this).getLogicalId( + firehoseDestinationBucket.node.defaultChild as s3.CfnBucket + )}.Arn>/*`, + ], + }, + ], + true // applyToChildren = true, so that it applies to policies created for the role. + ); + + this.dataRepository.grantReadWriteData(this.dataAggregator); + firehoseDestinationBucket.grantRead(this.dataAggregator); + + firehoseDestinationBucket.addObjectCreatedNotification( + new s3n.LambdaDestination(this.dataAggregator) + ); + + NagSuppressions.addResourceSuppressions( + [ + cdk.Stack.of(scope).node.findChild( + // logicalId for cdk-managed resource: + // https://github.com/aws/aws-cdk/blob/6a7a24afcc1ebebf71c267b890732a455e865cc8/packages/aws-cdk-lib/aws-s3/lib/notifications-resource/notifications-resource-handler.ts#L39 + 'BucketNotificationsHandler050a0587b7544547bf325f094a3db834' + ), + ], + [ + { + id: 'AwsSolutions-IAM4', + reason: + 'Where required, the Control Plane API uses a custom authorizer. It does not use a cognito authorizer.', + }, + { + id: 'AwsSolutions-IAM5', + reason: + 'Where required, the Control Plane API uses a custom authorizer. It does not use a cognito authorizer.', + }, + ], + true // applyToChildren = true, so that it applies to the APIGW role created by cdk in the controlPlaneAPI construct + ); + } +} diff --git a/src/control-plane/ingestor-aggregator/index.ts b/src/control-plane/ingestor-aggregator/index.ts new file mode 100644 index 0000000..7510a23 --- /dev/null +++ b/src/control-plane/ingestor-aggregator/index.ts @@ -0,0 +1,5 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +export * from './ingestor-aggregator-interface'; +export * from './firehose-ingestor-aggregator'; diff --git a/src/control-plane/ingestor-aggregator/ingestor-aggregator-interface.ts b/src/control-plane/ingestor-aggregator/ingestor-aggregator-interface.ts new file mode 100644 index 0000000..dae4cb8 --- /dev/null +++ b/src/control-plane/ingestor-aggregator/ingestor-aggregator-interface.ts @@ -0,0 +1,26 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import * as dynamodb from 'aws-cdk-lib/aws-dynamodb'; +import * as lambda from 'aws-cdk-lib/aws-lambda'; + +/** + * Encapsulates the list of properties for a IDataIngestorAggregator. + */ +export interface IDataIngestorAggregator { + /** + * The table containing the aggregated data. + */ + readonly dataRepository: dynamodb.ITable; + + /** + * The function responsible for aggregating the raw data coming in + * via the dataIngestor. + */ + readonly dataAggregator: lambda.IFunction; + + /** + * The ingestor responsible for accepting and storing the incoming data. + */ + readonly dataIngestorName: string; +} diff --git a/src/control-plane/integ.default.ts b/src/control-plane/integ.default.ts index eebd0b6..a9f5867 100644 --- a/src/control-plane/integ.default.ts +++ b/src/control-plane/integ.default.ts @@ -20,11 +20,6 @@ export class IntegStack extends cdk.Stack { // for event bridge communication const idpName = 'COGNITO'; const systemAdminRoleName = 'SystemAdmin'; - const applicationPlaneEventSource = 'testApplicationPlaneEventSource'; - const provisioningDetailType = 'testProvisioningDetailType'; - const controlPlaneEventSource = 'testControlPlaneEventSource'; - const onboardingDetailType = 'Onboarding'; - const offboardingDetailType = 'Offboarding'; const cognitoAuth = new CognitoAuth(this, 'CognitoAuth', { idpName: idpName, @@ -36,11 +31,6 @@ export class IntegStack extends cdk.Stack { const controlPlane = new ControlPlane(this, 'ControlPlane', { auth: cognitoAuth, - applicationPlaneEventSource: applicationPlaneEventSource, - provisioningDetailType: provisioningDetailType, - controlPlaneEventSource: controlPlaneEventSource, - onboardingDetailType: onboardingDetailType, - offboardingDetailType: offboardingDetailType, }); const eventBus = EventBus.fromEventBusArn( @@ -54,7 +44,10 @@ export class IntegStack extends cdk.Stack { eventBus: eventBus, enabled: true, eventPattern: { - source: [controlPlaneEventSource, applicationPlaneEventSource], + source: [ + controlPlane.eventManager.controlPlaneEventSource, + controlPlane.eventManager.applicationPlaneEventSource, + ], }, }); diff --git a/src/control-plane/services.ts b/src/control-plane/services.ts index b640f84..1f6cfbd 100644 --- a/src/control-plane/services.ts +++ b/src/control-plane/services.ts @@ -3,21 +3,18 @@ import * as path from 'path'; import { PythonFunction } from '@aws-cdk/aws-lambda-python-alpha'; -import { Duration } from 'aws-cdk-lib'; -import { EventBus } from 'aws-cdk-lib/aws-events'; +import { Duration, Stack } from 'aws-cdk-lib'; import { Role, ServicePrincipal, ManagedPolicy } from 'aws-cdk-lib/aws-iam'; import { Runtime, LayerVersion, Function } from 'aws-cdk-lib/aws-lambda'; import { NagSuppressions } from 'cdk-nag'; import { Construct } from 'constructs'; import { Tables } from './tables'; +import { DetailType, EventManager } from '../utils'; export interface ServicesProps { - readonly eventBus: EventBus; - readonly idpDetails: string; readonly lambdaLayer: LayerVersion; readonly tables: Tables; - readonly onboardingDetailType: string; - readonly controlPlaneEventSource: string; + readonly eventManager: EventManager; } export class Services extends Construct { @@ -31,7 +28,7 @@ export class Services extends Construct { }); props.tables.tenantDetails.grantReadWriteData(tenantManagementExecRole); - props.eventBus.grantPutEventsTo(tenantManagementExecRole); + props.eventManager.eventBus.grantPutEventsTo(tenantManagementExecRole); tenantManagementExecRole.addManagedPolicy( ManagedPolicy.fromAwsManagedPolicyName('service-role/AWSLambdaBasicExecutionRole') @@ -65,18 +62,29 @@ export class Services extends Construct { true // applyToChildren = true, so that it applies to policies created for the role. ); + // https://docs.powertools.aws.dev/lambda/python/2.31.0/#lambda-layer + const lambdaPowerToolsLayerARN = `arn:aws:lambda:${ + Stack.of(this).region + }:017000801446:layer:AWSLambdaPowertoolsPythonV2:59`; + const tenantManagementServices = new PythonFunction(this, 'TenantManagementServices', { - entry: path.join(__dirname, '../../resources/functions/'), + entry: path.join(__dirname, '../../resources/functions/tenant-management'), runtime: Runtime.PYTHON_3_12, index: 'tenant_management.py', handler: 'lambda_handler', timeout: Duration.seconds(60), role: tenantManagementExecRole, - layers: [props.lambdaLayer], + layers: [ + LayerVersion.fromLayerVersionArn(this, 'LambdaPowerTools', lambdaPowerToolsLayerARN), + ], environment: { - EVENTBUS_NAME: props.eventBus.eventBusName, - EVENT_SOURCE: props.controlPlaneEventSource, + EVENTBUS_NAME: props.eventManager.eventBus.eventBusName, + EVENT_SOURCE: props.eventManager.controlPlaneEventSource, TENANT_DETAILS_TABLE: props.tables.tenantDetails.tableName, + ONBOARDING_DETAIL_TYPE: DetailType.ONBOARDING_REQUEST, + OFFBOARDING_DETAIL_TYPE: DetailType.OFFBOARDING_REQUEST, + ACTIVATE_DETAIL_TYPE: DetailType.ACTIVATE_REQUEST, + DEACTIVATE_DETAIL_TYPE: DetailType.DEACTIVATE_SUCCESS, }, }); diff --git a/src/control-plane/tenant-config/tenant-config-service.ts b/src/control-plane/tenant-config/tenant-config-service.ts index 6c87876..018ad38 100644 --- a/src/control-plane/tenant-config/tenant-config-service.ts +++ b/src/control-plane/tenant-config/tenant-config-service.ts @@ -41,7 +41,9 @@ export class TenantConfigService extends Construct { TENANT_NAME_COLUMN: props.tenantDetailsTenantNameColumn, TENANT_CONFIG_COLUMN: props.tenantDetailsTenantConfigColumn, }, - logRetention: cdk.aws_logs.RetentionDays.FIVE_DAYS, + logGroup: new cdk.aws_logs.LogGroup(this, 'BillingIngestorLogGroup', { + retention: cdk.aws_logs.RetentionDays.FIVE_DAYS, + }), layers: [ lambda.LayerVersion.fromLayerVersionArn( this, diff --git a/src/core-app-plane/bash-job-orchestrator.ts b/src/core-app-plane/bash-job-orchestrator.ts index 46bb463..1287420 100644 --- a/src/core-app-plane/bash-job-orchestrator.ts +++ b/src/core-app-plane/bash-job-orchestrator.ts @@ -35,12 +35,13 @@ export interface BashJobOrchestratorProps extends cdk.StackProps { /** * Environment variables to import into the bash job from event details field. */ - readonly importedVariables?: string[]; + readonly environmentStringVariablesFromIncomingEvent?: string[]; + readonly environmentJSONVariablesFromIncomingEvent?: string[]; /** * Environment variables to export into the outgoing event once the bash job has finished. */ - readonly exportedVariables?: string[]; + readonly environmentVariablesToOutgoingEvent?: string[]; /** * The BashJobRunner to execute as part of this BashJobOrchestrator. @@ -73,13 +74,20 @@ export class BashJobOrchestrator extends Construct { [name: string]: codebuild.BuildEnvironmentVariable; } = {}; - props.importedVariables?.forEach((importedVar: string) => { + props.environmentStringVariablesFromIncomingEvent?.forEach((importedVar: string) => { environmentVariablesOverride[importedVar] = { type: codebuild.BuildEnvironmentVariableType.PLAINTEXT, value: sfn.JsonPath.stringAt(`$.detail.${importedVar}`), }; }); + props.environmentJSONVariablesFromIncomingEvent?.forEach((importedVar: string) => { + environmentVariablesOverride[importedVar] = { + type: codebuild.BuildEnvironmentVariableType.PLAINTEXT, + value: sfn.JsonPath.jsonToString(sfn.JsonPath.objectAt(`$.detail.${importedVar}`)), + }; + }); + const stateMachineLogGroup = new logs.LogGroup(this, 'stateMachineLogGroup', { removalPolicy: cdk.RemovalPolicy.DESTROY, retention: logs.RetentionDays.THREE_DAYS, @@ -101,7 +109,7 @@ export class BashJobOrchestrator extends Construct { tenantId: sfn.JsonPath.stringAt(`$.detail.tenantId`), tenantOutput: {}, }; - props.exportedVariables?.forEach((exportedVar: string) => { + props.environmentVariablesToOutgoingEvent?.forEach((exportedVar: string) => { exportedVarObj.tenantOutput[exportedVar] = sfn.JsonPath.arrayGetItem( sfn.JsonPath.listAt( `$.startProvisioningCodeBuild.Build.ExportedEnvironmentVariables[?(@.Name==${exportedVar})].Value` diff --git a/src/core-app-plane/bash-job-runner.ts b/src/core-app-plane/bash-job-runner.ts index 355e5ce..e17934c 100644 --- a/src/core-app-plane/bash-job-runner.ts +++ b/src/core-app-plane/bash-job-runner.ts @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 import * as codebuild from 'aws-cdk-lib/aws-codebuild'; -import { IEventBus, EventField, IRuleTarget, RuleTargetInput } from 'aws-cdk-lib/aws-events'; +import { EventField, IRuleTarget, RuleTargetInput } from 'aws-cdk-lib/aws-events'; import * as targets from 'aws-cdk-lib/aws-events-targets'; import * as iam from 'aws-cdk-lib/aws-iam'; import * as kms from 'aws-cdk-lib/aws-kms'; @@ -28,21 +28,6 @@ export interface BashJobRunnerProps { */ readonly script: string; - /** - * The eventBus to submit the outgoing event to once the BashJobRunner has finished. - */ - readonly eventBus: IEventBus; - - /** - * The source of the event that will be emitted once the BashJobRunner has finished. - */ - readonly outgoingEventSource: string; - - /** - * The detail type of the event that will be emitted once the BashJobRunner has finished. - */ - readonly outgoingEventDetailType: string; - /** * The bash script to run after the main script has completed. */ @@ -51,12 +36,12 @@ export interface BashJobRunnerProps { /** * The environment variables to import into the BashJobRunner from event details field. */ - readonly importedVariables?: string[]; + readonly environmentVariablesFromIncomingEvent?: string[]; /** * The environment variables to export into the outgoing event once the BashJobRunner has finished. */ - readonly exportedVariables?: string[]; + readonly environmentVariablesToOutgoingEvent?: string[]; /** * The variables to pass into the codebuild BashJobRunner. @@ -86,7 +71,7 @@ export class BashJobRunner extends Construct { * The environment variables to export into the outgoing event once the BashJobRunner has finished. * @attribute */ - public readonly exportedVariables?: string[]; + public readonly environmentVariablesToOutgoingEvent?: string[]; constructor(scope: Construct, id: string, props: BashJobRunnerProps) { super(scope, id); @@ -96,7 +81,7 @@ export class BashJobRunner extends Construct { type: codebuild.BuildEnvironmentVariableType; }[] = []; - props.importedVariables?.forEach((importedVariable: string) => { + props.environmentVariablesFromIncomingEvent?.forEach((importedVariable: string) => { environmentVariablesOverride.push({ name: importedVariable, value: EventField.fromPath(`$.detail.${importedVariable}`), @@ -117,7 +102,7 @@ export class BashJobRunner extends Construct { } } - this.exportedVariables = props.exportedVariables; + this.environmentVariablesToOutgoingEvent = props.environmentVariablesToOutgoingEvent; const codeBuildProjectEncryptionKey = new kms.Key( scope, @@ -138,8 +123,8 @@ export class BashJobRunner extends Construct { version: '0.2', env: { shell: 'bash', - ...(props.exportedVariables && { - 'exported-variables': props.exportedVariables, + ...(props.environmentVariablesToOutgoingEvent && { + 'exported-variables': props.environmentVariablesToOutgoingEvent, }), }, phases: { @@ -186,7 +171,6 @@ export class BashJobRunner extends Construct { document: props.permissions, }) ); - props.eventBus.grantPutEventsTo(this.codebuildProject); this.eventTarget = new targets.CodeBuildProject(this.codebuildProject, { event: RuleTargetInput.fromObject({ diff --git a/src/core-app-plane/core-app-plane.ts b/src/core-app-plane/core-app-plane.ts index 05bcf9e..0b42182 100644 --- a/src/core-app-plane/core-app-plane.ts +++ b/src/core-app-plane/core-app-plane.ts @@ -8,7 +8,7 @@ import { Construct } from 'constructs'; import { BashJobOrchestrator } from './bash-job-orchestrator'; import { BashJobRunner } from './bash-job-runner'; import { DestroyPolicySetter } from '../cdk-aspect/destroy-policy-setter'; -import { EventManager, setTemplateDesc } from '../utils'; +import { EventManager, EventMetadata, DetailType, setTemplateDesc } from '../utils'; /** * Provides metadata for outgoing events. @@ -22,7 +22,7 @@ export interface OutgoingEventMetadata { /** * The source to set in the outgoing event. * - * @default CoreApplicationPlaneProps.applicationNamePlaneSource + * @default CoreApplicationPlaneProps.applicationPlaneEventSource */ readonly source?: string; } @@ -39,7 +39,7 @@ export interface IncomingEventMetadata { /** * The list of sources to listen for in the incoming event. * - * @default CoreApplicationPlaneProps.controlPlaneSource + * @default CoreApplicationPlaneProps.controlPlaneEventSource */ readonly source?: string[]; } @@ -64,14 +64,14 @@ export interface CoreApplicationPlaneJobRunnerProps { readonly script: string; /** - * The IncomingEventMetadata to use when listening for the event that will trigger this CoreApplicationPlaneJobRunner. + * The incoming event DetailType that triggers this job. */ - readonly incomingEvent: IncomingEventMetadata; + readonly incomingEvent: DetailType; /** - * The OutgoingEventMetadata to use when submitting a new event after this CoreApplicationPlaneJobRunner has executed. + * The outgoing event DetailType that is emitted upon job completion. */ - readonly outgoingEvent: OutgoingEventMetadata; + readonly outgoingEvent: DetailType; /** * The bash script to run after the main script has completed. @@ -80,13 +80,21 @@ export interface CoreApplicationPlaneJobRunnerProps { /** * The environment variables to import into the CoreApplicationPlaneJobRunner from event details field. + * This argument consists of the names of only string type variables. Ex. 'test' */ - readonly importedVariables?: string[]; + readonly environmentStringVariablesFromIncomingEvent?: string[]; + + /** + * The environment variables to import into the CoreApplicationPlaneJobRunner from event details field. + * This argument consists of the names of only JSON-formatted string type variables. + * Ex. '{"test": 2}' + */ + readonly environmentJSONVariablesFromIncomingEvent?: string[]; /** * The environment variables to export into the outgoing event once the CoreApplicationPlaneJobRunner has finished. */ - readonly exportedVariables?: string[]; + readonly environmentVariablesToOutgoingEvent?: string[]; /** * The variables to pass into the codebuild CoreApplicationPlaneJobRunner. @@ -110,18 +118,20 @@ export interface CoreApplicationPlaneProps { * The source to use when listening for events coming from the SBT control plane. * This is used as the default if the IncomingEventMetadata source field is not set. */ - readonly controlPlaneSource: string; + readonly controlPlaneEventSource?: string; /** * The source to use for outgoing events that will be placed on the EventBus. * This is used as the default if the OutgoingEventMetadata source field is not set. */ - readonly applicationNamePlaneSource: string; + readonly applicationPlaneEventSource?: string; /** * The list of JobRunner definitions to create. */ readonly jobRunnerPropsList?: CoreApplicationPlaneJobRunnerProps[]; + + readonly eventMetadata?: EventMetadata; } /** @@ -131,6 +141,8 @@ export interface CoreApplicationPlaneProps { * and respond to events created by the control plane. */ export class CoreApplicationPlane extends Construct { + readonly eventManager: EventManager; + constructor(scope: Construct, id: string, props: CoreApplicationPlaneProps) { super(scope, id); setTemplateDesc(this, 'SaaS Builder Toolkit - CoreApplicationPlane (uksb-1tupboc57)'); @@ -139,40 +151,48 @@ export class CoreApplicationPlane extends Construct { const eventBus = EventBus.fromEventBusArn(this, 'eventBus', props.eventBusArn); - const eventManager = new EventManager(this, 'EventManager', { + this.eventManager = new EventManager(this, 'EventManager', { eventBus: eventBus, + eventMetadata: props.eventMetadata, + applicationPlaneEventSource: props.applicationPlaneEventSource, + controlPlaneEventSource: props.controlPlaneEventSource, }); props.jobRunnerPropsList?.forEach((jobRunnerProps) => { + // Only BashJobOrchestrator requires differentiating between + // strings and JSON variables pulled from the incoming event. + let envVarsFromIncomingEvent: string[] = []; + if (jobRunnerProps.environmentStringVariablesFromIncomingEvent) { + envVarsFromIncomingEvent.concat(jobRunnerProps.environmentStringVariablesFromIncomingEvent); + } + + if (jobRunnerProps.environmentJSONVariablesFromIncomingEvent) { + envVarsFromIncomingEvent.concat(jobRunnerProps.environmentJSONVariablesFromIncomingEvent); + } + let job = new BashJobRunner(this, jobRunnerProps.name, { name: jobRunnerProps.name, permissions: jobRunnerProps.permissions, script: jobRunnerProps.script, postScript: jobRunnerProps.postScript, - importedVariables: jobRunnerProps.importedVariables, - exportedVariables: jobRunnerProps.exportedVariables, + environmentVariablesFromIncomingEvent: envVarsFromIncomingEvent, + environmentVariablesToOutgoingEvent: jobRunnerProps.environmentVariablesToOutgoingEvent, scriptEnvironmentVariables: jobRunnerProps.scriptEnvironmentVariables, - eventBus: eventBus, - outgoingEventDetailType: jobRunnerProps.outgoingEvent.detailType, - outgoingEventSource: - jobRunnerProps.outgoingEvent.source || props.applicationNamePlaneSource, }); let jobOrchestrator = new BashJobOrchestrator(this, `${jobRunnerProps.name}-orchestrator`, { targetEventBus: eventBus, - detailType: jobRunnerProps.outgoingEvent.detailType, - eventSource: jobRunnerProps.outgoingEvent.source || props.applicationNamePlaneSource, - exportedVariables: jobRunnerProps.exportedVariables, - importedVariables: jobRunnerProps.importedVariables, + detailType: jobRunnerProps.outgoingEvent, + eventSource: this.eventManager.supportedEvents[jobRunnerProps.outgoingEvent], + environmentVariablesToOutgoingEvent: jobRunnerProps.environmentVariablesToOutgoingEvent, + environmentStringVariablesFromIncomingEvent: + jobRunnerProps.environmentStringVariablesFromIncomingEvent, + environmentJSONVariablesFromIncomingEvent: + jobRunnerProps.environmentJSONVariablesFromIncomingEvent, bashJobRunner: job, }); - eventManager.addRuleWithTarget( - jobRunnerProps.name, - jobRunnerProps.incomingEvent.source || [props.controlPlaneSource], - jobRunnerProps.incomingEvent.detailType, - jobOrchestrator.eventTarget - ); + this.eventManager.addTargetToEvent(jobRunnerProps.incomingEvent, jobOrchestrator.eventTarget); }); } } diff --git a/src/core-app-plane/integ.default.ts b/src/core-app-plane/integ.default.ts index 7196d39..70b00ab 100644 --- a/src/core-app-plane/integ.default.ts +++ b/src/core-app-plane/integ.default.ts @@ -4,11 +4,12 @@ import * as cdk from 'aws-cdk-lib'; import { EventBus, Rule } from 'aws-cdk-lib/aws-events'; import * as targets from 'aws-cdk-lib/aws-events-targets'; -import { PolicyDocument } from 'aws-cdk-lib/aws-iam'; +import { Effect, PolicyDocument, PolicyStatement } from 'aws-cdk-lib/aws-iam'; import { LogGroup, RetentionDays } from 'aws-cdk-lib/aws-logs'; import { AwsSolutionsChecks, NagSuppressions } from 'cdk-nag'; -import { CoreApplicationPlane } from '.'; +import { CoreApplicationPlane, CoreApplicationPlaneJobRunnerProps } from '.'; import { DestroyPolicySetter } from '../cdk-aspect/destroy-policy-setter'; +import { DetailType } from '../utils'; export interface IntegStackProps extends cdk.StackProps { eventBusArn?: string; @@ -18,13 +19,6 @@ export class IntegStack extends cdk.Stack { constructor(scope: cdk.App, id: string, props?: IntegStackProps) { super(scope, id, props); - const controlPlaneSource = 'testControlPlaneEventSource'; - const applicationPlaneSource = 'testApplicationPlaneEventSource'; - const provisioningDetailType = 'testProvisioningDetailType'; - const onboardingDetailType = 'Onboarding'; - const offboardingDetailType = 'Offboarding'; - const deprovisioningDetailType = 'testDeprovisioningDetailType'; - let eventBus; if (props?.eventBusArn) { eventBus = EventBus.fromEventBusArn(this, 'EventBus', props.eventBusArn); @@ -32,62 +26,21 @@ export class IntegStack extends cdk.Stack { eventBus = new EventBus(this, 'EventBus'); } - const eventBusWatcherRule = new Rule(this, 'EventBusWatcherRule', { - eventBus: eventBus, - enabled: true, - eventPattern: { - source: [controlPlaneSource, applicationPlaneSource], - }, - }); - - NagSuppressions.addResourceSuppressions( - eventBusWatcherRule, - [ - { - id: 'AwsSolutions-IAM4', - reason: 'Suppress error from resource created for testing.', - appliesTo: [ - 'Policy::arn::iam::aws:policy/service-role/AWSLambdaBasicExecutionRole', - ], - }, - { - id: 'AwsSolutions-IAM5', - reason: 'Suppress error from resource created for testing.', - appliesTo: ['Resource::*'], - }, - ], - true // applyToChildren = true, so that it applies to resources created by the rule. Ex. lambda role. - ); - - eventBusWatcherRule.addTarget( - new targets.CloudWatchLogGroup( - new LogGroup(this, 'EventBusWatcherLogGroup', { - removalPolicy: cdk.RemovalPolicy.DESTROY, - retention: RetentionDays.ONE_WEEK, - }) - ) - ); - - const provisioningJobRunnerProps = { + const provisioningJobRunnerProps: CoreApplicationPlaneJobRunnerProps = { name: 'provisioning', - permissions: PolicyDocument.fromJson( - JSON.parse(` -{ - "Version":"2012-10-17", - "Statement":[ - { - "Action":[ - "cloudformation:CreateStack", - "cloudformation:DescribeStacks", - "s3:CreateBucket" + permissions: new PolicyDocument({ + statements: [ + new PolicyStatement({ + actions: [ + 'cloudformation:CreateStack', + 'cloudformation:DescribeStacks', + 's3:CreateBucket', + ], + resources: ['*'], + effect: Effect.ALLOW, + }), ], - "Resource":"*", - "Effect":"Allow" - } - ] -} -`) - ), + }), script: ` echo "starting..." @@ -126,45 +79,44 @@ export tenantConfig=$(jq --arg SAAS_APP_USERPOOL_ID "MY_SAAS_APP_USERPOOL_ID" \ -n '{"userPoolId":$SAAS_APP_USERPOOL_ID,"appClientId":$SAAS_APP_CLIENT_ID,"apiGatewayUrl":$API_GATEWAY_URL}') echo $tenantConfig +export tenantStatus="created" echo "done!" `, postScript: '', - importedVariables: ['tenantId', 'tier'], - exportedVariables: ['tenantS3Bucket', 'someOtherVariable', 'tenantConfig'], + environmentStringVariablesFromIncomingEvent: ['tenantId', 'tier', 'tenantName', 'email'], + environmentJSONVariablesFromIncomingEvent: ['prices'], + environmentVariablesToOutgoingEvent: [ + 'tenantS3Bucket', + 'someOtherVariable', + 'tenantConfig', + 'tenantStatus', + 'prices', // added so we don't lose it for targets beyond provisioning (ex. billing) + 'tenantName', // added so we don't lose it for targets beyond provisioning (ex. billing) + 'email', // added so we don't lose it for targets beyond provisioning (ex. billing) + ], scriptEnvironmentVariables: { TEST: 'test', }, - outgoingEvent: { - source: applicationPlaneSource, - detailType: provisioningDetailType, - }, - incomingEvent: { - source: [controlPlaneSource], - detailType: [onboardingDetailType], - }, + outgoingEvent: DetailType.PROVISION_SUCCESS, + incomingEvent: DetailType.ONBOARDING_REQUEST, }; - const deprovisioningJobRunnerProps = { + const deprovisioningJobRunnerProps: CoreApplicationPlaneJobRunnerProps = { name: 'deprovisioning', - permissions: PolicyDocument.fromJson( - JSON.parse(` -{ - "Version":"2012-10-17", - "Statement":[ - { - "Action":[ - "cloudformation:DeleteStack", - "cloudformation:DescribeStacks", - "s3:DeleteBucket" + permissions: new PolicyDocument({ + statements: [ + new PolicyStatement({ + actions: [ + 'cloudformation:DeleteStack', + 'cloudformation:DescribeStacks', + 's3:DeleteBucket', + ], + resources: ['*'], + effect: Effect.ALLOW, + }), ], - "Resource":"*", - "Effect":"Allow" - } - ] -} -`) - ), + }), script: ` echo "starting..." @@ -173,26 +125,58 @@ echo "tenantId: $tenantId" aws cloudformation delete-stack --stack-name "tenantTemplateStack-\${tenantId}" aws cloudformation wait stack-delete-complete --stack-name "tenantTemplateStack-\${tenantId}" export status="deleted stack: tenantTemplateStack-\${tenantId}" +export tenantStatus="deleted" echo "done!" `, - importedVariables: ['tenantId'], - exportedVariables: ['status'], - outgoingEvent: { - source: applicationPlaneSource, - detailType: deprovisioningDetailType, - }, - incomingEvent: { - source: [controlPlaneSource], - detailType: [offboardingDetailType], - }, + environmentStringVariablesFromIncomingEvent: ['tenantId'], + environmentVariablesToOutgoingEvent: ['tenantStatus'], + outgoingEvent: DetailType.DEPROVISION_SUCCESS, + incomingEvent: DetailType.OFFBOARDING_REQUEST, }; - new CoreApplicationPlane(this, 'CoreApplicationPlane', { + const coreApplicationPlane = new CoreApplicationPlane(this, 'CoreApplicationPlane', { eventBusArn: eventBus.eventBusArn, - controlPlaneSource: controlPlaneSource, - applicationNamePlaneSource: applicationPlaneSource, jobRunnerPropsList: [provisioningJobRunnerProps, deprovisioningJobRunnerProps], }); + + const eventBusWatcherRule = new Rule(this, 'EventBusWatcherRule', { + eventBus: eventBus, + enabled: true, + eventPattern: { + source: [ + coreApplicationPlane.eventManager.controlPlaneEventSource, + coreApplicationPlane.eventManager.applicationPlaneEventSource, + ], + }, + }); + + NagSuppressions.addResourceSuppressions( + eventBusWatcherRule, + [ + { + id: 'AwsSolutions-IAM4', + reason: 'Suppress error from resource created for testing.', + appliesTo: [ + 'Policy::arn::iam::aws:policy/service-role/AWSLambdaBasicExecutionRole', + ], + }, + { + id: 'AwsSolutions-IAM5', + reason: 'Suppress error from resource created for testing.', + appliesTo: ['Resource::*'], + }, + ], + true // applyToChildren = true, so that it applies to resources created by the rule. Ex. lambda role. + ); + + eventBusWatcherRule.addTarget( + new targets.CloudWatchLogGroup( + new LogGroup(this, 'EventBusWatcherLogGroup', { + removalPolicy: cdk.RemovalPolicy.DESTROY, + retention: RetentionDays.ONE_WEEK, + }) + ) + ); } } @@ -262,7 +246,7 @@ NagSuppressions.addResourceSuppressionsByPath( [ { id: 'AwsSolutions-IAM4', - reason: 'Suppress errors generated by updates to cdk-managed CodeBuild Project role.', + reason: 'Suppress error from resource created for testing.', appliesTo: [ 'Policy::arn::iam::aws:policy/service-role/AWSLambdaBasicExecutionRole', ], diff --git a/src/utils/event-manager.ts b/src/utils/event-manager.ts index ea78f66..256a443 100644 --- a/src/utils/event-manager.ts +++ b/src/utils/event-manager.ts @@ -4,6 +4,42 @@ import { IEventBus, Rule, IRuleTarget } from 'aws-cdk-lib/aws-events'; import { Construct } from 'constructs'; +/** + * Provides an easy way of accessing event DetailTypes. + * Note that the string represents the detailTypes used in + * events sent across the EventBus. + */ +export enum DetailType { + ONBOARDING_REQUEST = 'onboardingRequest', + ONBOARDING_SUCCESS = 'onboardingSuccess', + ONBOARDING_FAILURE = 'onboardingFailure', + OFFBOARDING_REQUEST = 'offboardingRequest', + OFFBOARDING_SUCCESS = 'offboardingSuccess', + OFFBOARDING_FAILURE = 'offboardingFailure', + PROVISION_SUCCESS = 'provisionSuccess', + PROVISION_FAILURE = 'provisionFailure', + DEPROVISION_SUCCESS = 'deprovisionSuccess', + DEPROVISION_FAILURE = 'deprovisionFailure', + BILLING_SUCCESS = 'billingSuccess', + BILLING_FAILURE = 'billingFailure', + ACTIVATE_REQUEST = 'activateRequest', + ACTIVATE_SUCCESS = 'activateSuccess', + ACTIVATE_FAILURE = 'activateFailure', + DEACTIVATE_REQUEST = 'deactivateRequest', + DEACTIVATE_SUCCESS = 'deactivateSuccess', + DEACTIVATE_FAILURE = 'deactivateFailure', +} + +/** + * Represents mapping between 'detailType' as key, + * and 'source' as value. + */ +export type EventMetadata = { + // key: Event 'detailType' -> val: Event 'source' + [key: string]: string; + // [key in DetailType]: string; // Causes this error: Only string-indexed map types are supported +}; + /** * Encapsulates the list of properties for an eventManager. */ @@ -12,39 +48,100 @@ export interface EventManagerProps { * The event bus to register new rules with. */ readonly eventBus: IEventBus; + + /** + * The EventMetadata to use to update the event defaults. + */ + readonly eventMetadata?: EventMetadata; + + /** + * The source to use when listening for events coming from the SBT control plane. + */ + readonly controlPlaneEventSource?: string; + + /** + * The source to use for outgoing events that will be placed on the EventBus. + */ + readonly applicationPlaneEventSource?: string; } /** * Provides an EventManager to help interact with the EventBus shared with the SBT control plane. */ export class EventManager extends Construct { + public readonly applicationPlaneEventSource: string = 'applicationPlaneEventSource'; + public readonly controlPlaneEventSource: string = 'controlPlaneEventSource'; + // sensible defaults so they are not required when instantiating control plane + + public readonly supportedEvents: EventMetadata = { + onboardingRequest: this.controlPlaneEventSource, + onboardingSuccess: this.applicationPlaneEventSource, + onboardingFailure: this.applicationPlaneEventSource, + offboardingRequest: this.controlPlaneEventSource, + offboardingSuccess: this.applicationPlaneEventSource, + offboardingFailure: this.applicationPlaneEventSource, + provisionSuccess: this.applicationPlaneEventSource, + provisionFailure: this.applicationPlaneEventSource, + deprovisionSuccess: this.applicationPlaneEventSource, + deprovisionFailure: this.applicationPlaneEventSource, + billingSuccess: this.controlPlaneEventSource, + billingFailure: this.controlPlaneEventSource, + activateRequest: this.controlPlaneEventSource, + activateSuccess: this.applicationPlaneEventSource, + activateFailure: this.applicationPlaneEventSource, + deactivateRequest: this.controlPlaneEventSource, + deactivateSuccess: this.applicationPlaneEventSource, + deactivateFailure: this.applicationPlaneEventSource, + }; + /** * The event bus to register new rules with. * @attribute */ - private readonly eventBus: IEventBus; + public readonly eventBus: IEventBus; + + private readonly connectedRules: Map = new Map(); + constructor(scope: Construct, id: string, props: EventManagerProps) { super(scope, id); this.eventBus = props.eventBus; + + this.applicationPlaneEventSource = + props.applicationPlaneEventSource || this.applicationPlaneEventSource; + this.controlPlaneEventSource = props.controlPlaneEventSource || this.controlPlaneEventSource; + + for (const key in this.supportedEvents) { + // update this.eventMetadata with any values passed in via props + if (props.eventMetadata && props.eventMetadata[key]) { + this.supportedEvents[key] = props.eventMetadata[key]; + } + } } /** - * Function to add a new rule and register a target for the newly added rule. + * Adds an IRuleTarget to an event. + * + * @param eventType The name of the event to add a target to. + * @param target The target that will be added to the event. */ - addRuleWithTarget( - ruleName: string, - eventDetailType: string[], - eventSource: string[], - target: IRuleTarget - ) { - const rule = new Rule(this, ruleName, { - eventBus: this.eventBus, - enabled: true, - eventPattern: { - source: eventDetailType, - detailType: eventSource, - }, - }); - rule.addTarget(target); + public addTargetToEvent(eventType: DetailType, target: IRuleTarget): void { + this.getOrCreateRule(eventType).addTarget(target); + } + + private getOrCreateRule(eventType: DetailType): Rule { + let rule = this.connectedRules.get(eventType); + + if (!rule) { + rule = new Rule(this, `${eventType}Rule`, { + eventBus: this.eventBus, + eventPattern: { + source: [this.supportedEvents[eventType]], + detailType: [eventType], + }, + }); + this.connectedRules.set(eventType, rule); + } + + return rule; } } diff --git a/test/control-plane.test.ts b/test/control-plane.test.ts index 6747a70..e5f749e 100644 --- a/test/control-plane.test.ts +++ b/test/control-plane.test.ts @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 import * as cdk from 'aws-cdk-lib'; -import { Annotations, Match, Template, Capture } from 'aws-cdk-lib/assertions'; +import { Annotations, Capture, Match, Template } from 'aws-cdk-lib/assertions'; import { AwsSolutionsChecks } from 'cdk-nag'; import { Construct } from 'constructs'; import { CognitoAuth, ControlPlane } from '../src/control-plane'; @@ -15,12 +15,6 @@ describe('No unsuppressed cdk-nag Warnings or Errors', () => { const systemAdminEmail = 'test@example.com'; const idpName = 'COGNITO'; const systemAdminRoleName = 'SystemAdmin'; - const applicationPlaneEventSource = 'testApplicationPlaneEventSource'; - const provisioningDetailType = 'testProvisioningDetailType'; - const controlPlaneEventSource = 'testControlPlaneEventSource'; - const onboardingDetailType = 'testOnboarding'; - const offboardingDetailType = 'testOffboarding'; - const cognitoAuth = new CognitoAuth(this, 'CognitoAuth', { idpName: idpName, systemAdminRoleName: systemAdminRoleName, @@ -29,11 +23,6 @@ describe('No unsuppressed cdk-nag Warnings or Errors', () => { new ControlPlane(this, 'ControlPlane', { auth: cognitoAuth, - applicationPlaneEventSource: applicationPlaneEventSource, - provisioningDetailType: provisioningDetailType, - controlPlaneEventSource: controlPlaneEventSource, - onboardingDetailType: onboardingDetailType, - offboardingDetailType: offboardingDetailType, }); } } @@ -59,7 +48,7 @@ describe('No unsuppressed cdk-nag Warnings or Errors', () => { }); }); -describe('ControlPlane without Description', () => { +describe('ControlPlane', () => { const app = new cdk.App(); interface TestStackProps extends cdk.StackProps { systemAdminEmail: string; @@ -71,11 +60,6 @@ describe('ControlPlane without Description', () => { // for event bridge communication const idpName = 'COGNITO'; const systemAdminRoleName = 'SystemAdmin'; - const applicationPlaneEventSource = 'testApplicationPlaneEventSource'; - const provisioningDetailType = 'testProvisioningDetailType'; - const controlPlaneEventSource = 'testControlPlaneEventSource'; - const onboardingDetailType = 'testOnboarding'; - const offboardingDetailType = 'testOffboarding'; const cognitoAuth = new CognitoAuth(this, 'CognitoAuth', { idpName: idpName, @@ -87,11 +71,6 @@ describe('ControlPlane without Description', () => { new ControlPlane(this, 'ControlPlane', { auth: cognitoAuth, - applicationPlaneEventSource: applicationPlaneEventSource, - provisioningDetailType: provisioningDetailType, - controlPlaneEventSource: controlPlaneEventSource, - onboardingDetailType: onboardingDetailType, - offboardingDetailType: offboardingDetailType, }); } } @@ -101,7 +80,7 @@ describe('ControlPlane without Description', () => { }); const template = Template.fromStack(controlPlaneTestStack); - it('should have exactly 1 target for every Event Rule', () => { + it('should have at least 1 target for every Event Rule', () => { const targetsCapture = new Capture(); template.allResourcesProperties('AWS::Events::Rule', { Targets: targetsCapture, diff --git a/test/core-app-plane.test.ts b/test/core-app-plane.test.ts index 6c28a5c..66bfbf2 100644 --- a/test/core-app-plane.test.ts +++ b/test/core-app-plane.test.ts @@ -8,6 +8,7 @@ import { PolicyDocument } from 'aws-cdk-lib/aws-iam'; import { AwsSolutionsChecks, NagSuppressions } from 'cdk-nag'; import { Construct, IConstruct } from 'constructs'; import { CoreApplicationPlane } from '../src/core-app-plane'; +import { DetailType } from '../src/utils'; class DestroyPolicySetter implements cdk.IAspect { public visit(node: IConstruct): void { @@ -25,17 +26,13 @@ describe('No unsuppressed cdk-nag Warnings or Errors', () => { const eventBus = new EventBus(this, 'EventBus'); new CoreApplicationPlane(this, 'CoreApplicationPlane', { eventBusArn: eventBus.eventBusArn, - controlPlaneSource: 'sbt-control-plane-api', - applicationNamePlaneSource: 'sbt-application-plane-api', + controlPlaneEventSource: 'sbt-control-plane-api', + applicationPlaneEventSource: 'sbt-application-plane-api', jobRunnerPropsList: [ { name: 'provisioning', - outgoingEvent: { - detailType: 'Onboarding', - }, - incomingEvent: { - detailType: ['Onboarding'], - }, + outgoingEvent: DetailType.PROVISION_SUCCESS, + incomingEvent: DetailType.ONBOARDING_REQUEST, permissions: PolicyDocument.fromJson( JSON.parse(`{ "Version":"2012-10-17", @@ -95,17 +92,13 @@ describe('CoreApplicationPlane', () => { const eventBus = new EventBus(this, 'EventBus'); new CoreApplicationPlane(this, 'CoreApplicationPlane', { eventBusArn: eventBus.eventBusArn, - controlPlaneSource: 'sbt-control-plane-api', - applicationNamePlaneSource: 'sbt-application-plane-api', + controlPlaneEventSource: 'sbt-control-plane-api', + applicationPlaneEventSource: 'sbt-application-plane-api', jobRunnerPropsList: [ { name: 'provisioning', - outgoingEvent: { - detailType: 'Onboarding', - }, - incomingEvent: { - detailType: ['Onboarding'], - }, + outgoingEvent: DetailType.PROVISION_SUCCESS, + incomingEvent: DetailType.ONBOARDING_REQUEST, permissions: PolicyDocument.fromJson( JSON.parse(`{ "Version":"2012-10-17", @@ -150,17 +143,13 @@ describe('CoreApplicationPlane', () => { const eventBus = new EventBus(this, 'EventBus'); const coreApplicationPlane = new CoreApplicationPlane(this, 'CoreApplicationPlane', { eventBusArn: eventBus.eventBusArn, - controlPlaneSource: 'sbt-control-plane-api', - applicationNamePlaneSource: 'sbt-application-plane-api', + controlPlaneEventSource: 'sbt-control-plane-api', + applicationPlaneEventSource: 'sbt-application-plane-api', jobRunnerPropsList: [ { name: 'provisioning', - outgoingEvent: { - detailType: 'Onboarding', - }, - incomingEvent: { - detailType: ['Onboarding'], - }, + outgoingEvent: DetailType.PROVISION_SUCCESS, + incomingEvent: DetailType.ONBOARDING_REQUEST, permissions: PolicyDocument.fromJson( JSON.parse(`{ "Version":"2012-10-17",