diff --git a/CHANGELOG.md b/CHANGELOG.md index 1625df07c..f9f908ccf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,14 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. +## [2.70.0](https://github.com/awslabs/aws-solutions-constructs/compare/v2.69.0...v2.70.0) (2024-09-18) + +Built on CDK v2.154.1 + +### Features + +* **aws-constructs-factories:** add cloudwatch alarms to factory output ([#1201](https://github.com/awslabs/aws-solutions-constructs/issues/1201)) ([5e49430](https://github.com/awslabs/aws-solutions-constructs/commit/5e49430c88a6e8a403fd0f35028d1619d03df8ed)) + ## [2.69.0](https://github.com/awslabs/aws-solutions-constructs/compare/v2.68.0...v2.69.0) (2024-09-11) Built on CDK v2.154.1 diff --git a/source/lerna.json b/source/lerna.json index df38c2782..44e1bb335 100644 --- a/source/lerna.json +++ b/source/lerna.json @@ -6,5 +6,5 @@ "./patterns/@aws-solutions-constructs/*" ], "rejectCycles": "true", - "version": "2.69.0" + "version": "2.70.0" } diff --git a/source/patterns/@aws-solutions-constructs/aws-constructs-factories/README.md b/source/patterns/@aws-solutions-constructs/aws-constructs-factories/README.md index cca150d67..cb876ee94 100755 --- a/source/patterns/@aws-solutions-constructs/aws-constructs-factories/README.md +++ b/source/patterns/@aws-solutions-constructs/aws-constructs-factories/README.md @@ -171,6 +171,8 @@ stateMachineFactory(id: string, props: StateMachineFactoryProps): StateMachineFa |:-------------|:----------------|-----------------| |stateMachineProps|[`sfn.StateMachineProps`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_stepfunctions.StateMachineProps.html)|The CDK properties that define the state machine. This property is required and must include a definitionBody or definition (definition is deprecated)| |logGroup?|[]`logs.LogGroup`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_logs.LogGroup.html)|An existing LogGroup to which the new state machine will write log entries. Default: none, the construct will create a new log group.| +|createCloudWatchAlarms?|boolean|Whether to create recommended CloudWatch alarms for the State Machine. Default: the alarms are created| +|cloudWatchAlarmsPrefix?|string|Creating multiple State Machines with one Factories construct will result in name collisions as the cloudwatch alarms originally had fixed resource ids. This value was added to avoid collisions while not making changes that would be destructive for existing stacks. Unless you are creating multiple State Machines using factories you can ignore it| # StateMachineFactoryResponse @@ -178,6 +180,7 @@ stateMachineFactory(id: string, props: StateMachineFactoryProps): StateMachineFa |:-------------|:----------------|-----------------| |stateMachineProps|[`sfn.StateMachineProps`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_stepfunctions.StateMachineProps.html)|| |logGroup|[]`logs.LogGroupProps`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_logs.LogGroupProps.html)|| +|cloudwatchAlarms?|[`cloudwatch.Alarm[]`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_cloudwatch.Alarm.html)|The alarms created by the factory (ExecutionFailed, ExecutionThrottled, ExecutionAborted)| # Default settings @@ -187,6 +190,10 @@ Out of the box implementation of the Construct without any override will set the * Configured to log to the new log group at LogLevel.ERROR * Amazon CloudWatch Logs Log Group * Log name is prefaced with /aws/vendedlogs/ to avoid resource policy [issues](https://docs.aws.amazon.com/step-functions/latest/dg/cw-logs.html#cloudwatch-iam-policy). The Log Group name is still created to be unique to the stack to avoid name collisions. +* CloudWatch alarms for: + * 1 or more failed executions + * 1 or more executions being throttled + * 1 or more executions being aborted # Architecture ![Architecture Diagram](sf-architecture.png) diff --git a/source/patterns/@aws-solutions-constructs/aws-constructs-factories/lib/index.ts b/source/patterns/@aws-solutions-constructs/aws-constructs-factories/lib/index.ts index 36cc015b7..947e053a9 100644 --- a/source/patterns/@aws-solutions-constructs/aws-constructs-factories/lib/index.ts +++ b/source/patterns/@aws-solutions-constructs/aws-constructs-factories/lib/index.ts @@ -19,6 +19,7 @@ import * as sqs from 'aws-cdk-lib/aws-sqs'; import * as kms from 'aws-cdk-lib/aws-kms'; import * as logs from 'aws-cdk-lib/aws-logs'; import * as defaults from '@aws-solutions-constructs/core'; +import * as cloudwatch from 'aws-cdk-lib/aws-cloudwatch'; export interface S3BucketFactoryProps { readonly bucketProps?: s3.BucketProps | any, @@ -44,6 +45,22 @@ export interface StateMachineFactoryProps { * Default: none, the construct will create a new log group. */ readonly logGroupProps?: logs.LogGroupProps + /** + * Whether to create recommended CloudWatch alarms + * + * @default - Alarms are created + */ + readonly createCloudWatchAlarms?: boolean, + /** + * Creating multiple State Machines in 1 stack with constructs will + * result in name collisions as the cloudwatch alarms originally had fixed resource ids. + * This value was added to avoid collisions while not making changes that would be + * destructive for existing stacks. Unless you are creating multiple State Machines using constructs + * you can ignore it. + * + * @default - undefined + */ + readonly cloudWatchAlarmsPrefix?: string } // Create a response specifically for the interface to avoid coupling client with internal implementation @@ -57,6 +74,10 @@ export interface StateMachineFactoryResponse { * The log group that will receive log messages from the state maching */ readonly logGroup: logs.ILogGroup + /* + * The alarms created by the factory (ExecutionFailed, ExecutionThrottled, ExecutionAborted) + */ + readonly cloudwatchAlarms?: cloudwatch.Alarm[]; } export interface SqsQueueFactoryProps { @@ -135,10 +156,16 @@ export class ConstructsFactories extends Construct { } public stateMachineFactory(id: string, props: StateMachineFactoryProps): StateMachineFactoryResponse { - const buildStateMachineResponse = defaults.buildStateMachine(this, id, props.stateMachineProps, props.logGroupProps); + const buildStateMachineResponse = defaults.buildStateMachine(this, id, { + stateMachineProps: props.stateMachineProps, + logGroupProps: props.logGroupProps, + createCloudWatchAlarms: props.createCloudWatchAlarms, + cloudWatchAlarmsPrefix: props.cloudWatchAlarmsPrefix + }); return { stateMachine: buildStateMachineResponse.stateMachine, - logGroup: buildStateMachineResponse.logGroup + logGroup: buildStateMachineResponse.logGroup, + cloudwatchAlarms: buildStateMachineResponse.cloudWatchAlarms }; } diff --git a/source/patterns/@aws-solutions-constructs/aws-constructs-factories/sf-architecture.png b/source/patterns/@aws-solutions-constructs/aws-constructs-factories/sf-architecture.png index 53c15f7ce..95717d223 100644 Binary files a/source/patterns/@aws-solutions-constructs/aws-constructs-factories/sf-architecture.png and b/source/patterns/@aws-solutions-constructs/aws-constructs-factories/sf-architecture.png differ diff --git a/source/patterns/@aws-solutions-constructs/aws-constructs-factories/test/stepfunctions/integ.facstp-defaults.js.snapshot/cdk.out b/source/patterns/@aws-solutions-constructs/aws-constructs-factories/test/stepfunctions/integ.facstp-defaults.js.snapshot/cdk.out index 1f0068d32..ff952427d 100644 --- a/source/patterns/@aws-solutions-constructs/aws-constructs-factories/test/stepfunctions/integ.facstp-defaults.js.snapshot/cdk.out +++ b/source/patterns/@aws-solutions-constructs/aws-constructs-factories/test/stepfunctions/integ.facstp-defaults.js.snapshot/cdk.out @@ -1 +1 @@ -{"version":"36.0.0"} \ No newline at end of file +{"version":"36.0.25"} \ No newline at end of file diff --git a/source/patterns/@aws-solutions-constructs/aws-constructs-factories/test/stepfunctions/integ.facstp-defaults.js.snapshot/facstp-defaults.assets.json b/source/patterns/@aws-solutions-constructs/aws-constructs-factories/test/stepfunctions/integ.facstp-defaults.js.snapshot/facstp-defaults.assets.json index 181a77f07..6c83da8db 100644 --- a/source/patterns/@aws-solutions-constructs/aws-constructs-factories/test/stepfunctions/integ.facstp-defaults.js.snapshot/facstp-defaults.assets.json +++ b/source/patterns/@aws-solutions-constructs/aws-constructs-factories/test/stepfunctions/integ.facstp-defaults.js.snapshot/facstp-defaults.assets.json @@ -1,7 +1,7 @@ { - "version": "36.0.0", + "version": "36.0.25", "files": { - "c7ecd4e8eaccb1e7f7416826559ce7fa1c58bb3a5fb8298e571ac6695d1d50b7": { + "c609c27e6e7fc30d831454d0ca67932fbbb0f7d66f09883e3fc1e4a0afe294b6": { "source": { "path": "facstp-defaults.template.json", "packaging": "file" @@ -9,7 +9,7 @@ "destinations": { "current_account-current_region": { "bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}", - "objectKey": "c7ecd4e8eaccb1e7f7416826559ce7fa1c58bb3a5fb8298e571ac6695d1d50b7.json", + "objectKey": "c609c27e6e7fc30d831454d0ca67932fbbb0f7d66f09883e3fc1e4a0afe294b6.json", "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}" } } diff --git a/source/patterns/@aws-solutions-constructs/aws-constructs-factories/test/stepfunctions/integ.facstp-defaults.js.snapshot/facstp-defaults.template.json b/source/patterns/@aws-solutions-constructs/aws-constructs-factories/test/stepfunctions/integ.facstp-defaults.js.snapshot/facstp-defaults.template.json index efc209758..bed066299 100644 --- a/source/patterns/@aws-solutions-constructs/aws-constructs-factories/test/stepfunctions/integ.facstp-defaults.js.snapshot/facstp-defaults.template.json +++ b/source/patterns/@aws-solutions-constructs/aws-constructs-factories/test/stepfunctions/integ.facstp-defaults.js.snapshot/facstp-defaults.template.json @@ -176,6 +176,69 @@ "UpdateReplacePolicy": "Delete", "DeletionPolicy": "Delete" }, + "targetExecutionFailedAlarm97DD1630": { + "Type": "AWS::CloudWatch::Alarm", + "Properties": { + "AlarmDescription": "Alarm for the number of executions that failed exceeded the threshold of 1. ", + "ComparisonOperator": "GreaterThanOrEqualToThreshold", + "Dimensions": [ + { + "Name": "StateMachineArn", + "Value": { + "Ref": "targetStateMachinetestsm59897C57" + } + } + ], + "EvaluationPeriods": 1, + "MetricName": "ExecutionsFailed", + "Namespace": "AWS/States", + "Period": 300, + "Statistic": "Sum", + "Threshold": 1 + } + }, + "targetExecutionThrottledAlarmCAA85FD5": { + "Type": "AWS::CloudWatch::Alarm", + "Properties": { + "AlarmDescription": "Alarm for the number of executions that throttled exceeded the threshold of 1. ", + "ComparisonOperator": "GreaterThanOrEqualToThreshold", + "Dimensions": [ + { + "Name": "StateMachineArn", + "Value": { + "Ref": "targetStateMachinetestsm59897C57" + } + } + ], + "EvaluationPeriods": 1, + "MetricName": "ExecutionThrottled", + "Namespace": "AWS/States", + "Period": 300, + "Statistic": "Sum", + "Threshold": 1 + } + }, + "targetExecutionAbortedAlarm70BA06C6": { + "Type": "AWS::CloudWatch::Alarm", + "Properties": { + "AlarmDescription": "Alarm for the number of executions that aborted exceeded the threshold of 1. ", + "ComparisonOperator": "GreaterThanOrEqualToThreshold", + "Dimensions": [ + { + "Name": "StateMachineArn", + "Value": { + "Ref": "targetStateMachinetestsm59897C57" + } + } + ], + "EvaluationPeriods": 1, + "MetricName": "ExecutionsAborted", + "Namespace": "AWS/States", + "Period": 300, + "Statistic": "Maximum", + "Threshold": 1 + } + }, "lambdatestServiceRoleF3BDB8FC": { "Type": "AWS::IAM::Role", "Properties": { diff --git a/source/patterns/@aws-solutions-constructs/aws-constructs-factories/test/stepfunctions/integ.facstp-defaults.js.snapshot/facstpdefaultsIntegDefaultTestDeployAssertFD717592.assets.json b/source/patterns/@aws-solutions-constructs/aws-constructs-factories/test/stepfunctions/integ.facstp-defaults.js.snapshot/facstpdefaultsIntegDefaultTestDeployAssertFD717592.assets.json index c89fdb7b7..7659f54db 100644 --- a/source/patterns/@aws-solutions-constructs/aws-constructs-factories/test/stepfunctions/integ.facstp-defaults.js.snapshot/facstpdefaultsIntegDefaultTestDeployAssertFD717592.assets.json +++ b/source/patterns/@aws-solutions-constructs/aws-constructs-factories/test/stepfunctions/integ.facstp-defaults.js.snapshot/facstpdefaultsIntegDefaultTestDeployAssertFD717592.assets.json @@ -1,5 +1,5 @@ { - "version": "36.0.0", + "version": "36.0.25", "files": { "21fbb51d7b23f6a6c262b46a9caee79d744a3ac019fd45422d988b96d44b2a22": { "source": { diff --git a/source/patterns/@aws-solutions-constructs/aws-constructs-factories/test/stepfunctions/integ.facstp-defaults.js.snapshot/integ.json b/source/patterns/@aws-solutions-constructs/aws-constructs-factories/test/stepfunctions/integ.facstp-defaults.js.snapshot/integ.json index 8e67ec928..165f552f6 100644 --- a/source/patterns/@aws-solutions-constructs/aws-constructs-factories/test/stepfunctions/integ.facstp-defaults.js.snapshot/integ.json +++ b/source/patterns/@aws-solutions-constructs/aws-constructs-factories/test/stepfunctions/integ.facstp-defaults.js.snapshot/integ.json @@ -1,5 +1,5 @@ { - "version": "36.0.0", + "version": "36.0.25", "testCases": { "facstp-defaults/Integ/DefaultTest": { "stacks": [ diff --git a/source/patterns/@aws-solutions-constructs/aws-constructs-factories/test/stepfunctions/integ.facstp-defaults.js.snapshot/manifest.json b/source/patterns/@aws-solutions-constructs/aws-constructs-factories/test/stepfunctions/integ.facstp-defaults.js.snapshot/manifest.json index 44a9bbbda..a8be07d8f 100644 --- a/source/patterns/@aws-solutions-constructs/aws-constructs-factories/test/stepfunctions/integ.facstp-defaults.js.snapshot/manifest.json +++ b/source/patterns/@aws-solutions-constructs/aws-constructs-factories/test/stepfunctions/integ.facstp-defaults.js.snapshot/manifest.json @@ -1,5 +1,5 @@ { - "version": "36.0.0", + "version": "36.0.24", "artifacts": { "facstpdefaultsIntegDefaultTestDeployAssertFD717592.assets": { "type": "cdk:asset-manifest", @@ -66,7 +66,7 @@ "validateOnSynth": false, "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-deploy-role-${AWS::AccountId}-${AWS::Region}", "cloudFormationExecutionRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-cfn-exec-role-${AWS::AccountId}-${AWS::Region}", - "stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}/c7ecd4e8eaccb1e7f7416826559ce7fa1c58bb3a5fb8298e571ac6695d1d50b7.json", + "stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}/c609c27e6e7fc30d831454d0ca67932fbbb0f7d66f09883e3fc1e4a0afe294b6.json", "requiresBootstrapStackVersion": 6, "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version", "additionalDependencies": [ @@ -106,6 +106,24 @@ "data": "targetStateMachinetestsm59897C57" } ], + "/facstp-defaults/target/ExecutionFailedAlarm/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "targetExecutionFailedAlarm97DD1630" + } + ], + "/facstp-defaults/target/ExecutionThrottledAlarm/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "targetExecutionThrottledAlarmCAA85FD5" + } + ], + "/facstp-defaults/target/ExecutionAbortedAlarm/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "targetExecutionAbortedAlarm70BA06C6" + } + ], "/facstp-defaults/lambdatest/ServiceRole/Resource": [ { "type": "aws:cdk:logicalId", diff --git a/source/patterns/@aws-solutions-constructs/aws-constructs-factories/test/stepfunctions/integ.facstp-defaults.js.snapshot/tree.json b/source/patterns/@aws-solutions-constructs/aws-constructs-factories/test/stepfunctions/integ.facstp-defaults.js.snapshot/tree.json index aabdf53f6..ec543b73f 100644 --- a/source/patterns/@aws-solutions-constructs/aws-constructs-factories/test/stepfunctions/integ.facstp-defaults.js.snapshot/tree.json +++ b/source/patterns/@aws-solutions-constructs/aws-constructs-factories/test/stepfunctions/integ.facstp-defaults.js.snapshot/tree.json @@ -47,13 +47,13 @@ }, "constructInfo": { "fqn": "aws-cdk-lib.aws_logs.CfnLogGroup", - "version": "2.150.0" + "version": "2.154.1" } } }, "constructInfo": { "fqn": "aws-cdk-lib.aws_logs.LogGroup", - "version": "2.150.0" + "version": "2.154.1" } }, "StateMachinetestsm": { @@ -69,7 +69,7 @@ "path": "facstp-defaults/target/StateMachinetestsm/Role/ImportRole", "constructInfo": { "fqn": "aws-cdk-lib.Resource", - "version": "2.150.0" + "version": "2.154.1" } }, "Resource": { @@ -94,7 +94,7 @@ }, "constructInfo": { "fqn": "aws-cdk-lib.aws_iam.CfnRole", - "version": "2.150.0" + "version": "2.154.1" } }, "DefaultPolicy": { @@ -162,19 +162,19 @@ }, "constructInfo": { "fqn": "aws-cdk-lib.aws_iam.CfnPolicy", - "version": "2.150.0" + "version": "2.154.1" } } }, "constructInfo": { "fqn": "aws-cdk-lib.aws_iam.Policy", - "version": "2.150.0" + "version": "2.154.1" } } }, "constructInfo": { "fqn": "aws-cdk-lib.aws_iam.Role", - "version": "2.150.0" + "version": "2.154.1" } }, "Resource": { @@ -227,19 +227,136 @@ }, "constructInfo": { "fqn": "aws-cdk-lib.aws_stepfunctions.CfnStateMachine", - "version": "2.150.0" + "version": "2.154.1" } } }, "constructInfo": { "fqn": "aws-cdk-lib.aws_stepfunctions.StateMachine", - "version": "2.150.0" + "version": "2.154.1" + } + }, + "ExecutionFailedAlarm": { + "id": "ExecutionFailedAlarm", + "path": "facstp-defaults/target/ExecutionFailedAlarm", + "children": { + "Resource": { + "id": "Resource", + "path": "facstp-defaults/target/ExecutionFailedAlarm/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::CloudWatch::Alarm", + "aws:cdk:cloudformation:props": { + "alarmDescription": "Alarm for the number of executions that failed exceeded the threshold of 1. ", + "comparisonOperator": "GreaterThanOrEqualToThreshold", + "dimensions": [ + { + "name": "StateMachineArn", + "value": { + "Ref": "targetStateMachinetestsm59897C57" + } + } + ], + "evaluationPeriods": 1, + "metricName": "ExecutionsFailed", + "namespace": "AWS/States", + "period": 300, + "statistic": "Sum", + "threshold": 1 + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_cloudwatch.CfnAlarm", + "version": "2.154.1" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_cloudwatch.Alarm", + "version": "2.154.1" + } + }, + "ExecutionThrottledAlarm": { + "id": "ExecutionThrottledAlarm", + "path": "facstp-defaults/target/ExecutionThrottledAlarm", + "children": { + "Resource": { + "id": "Resource", + "path": "facstp-defaults/target/ExecutionThrottledAlarm/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::CloudWatch::Alarm", + "aws:cdk:cloudformation:props": { + "alarmDescription": "Alarm for the number of executions that throttled exceeded the threshold of 1. ", + "comparisonOperator": "GreaterThanOrEqualToThreshold", + "dimensions": [ + { + "name": "StateMachineArn", + "value": { + "Ref": "targetStateMachinetestsm59897C57" + } + } + ], + "evaluationPeriods": 1, + "metricName": "ExecutionThrottled", + "namespace": "AWS/States", + "period": 300, + "statistic": "Sum", + "threshold": 1 + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_cloudwatch.CfnAlarm", + "version": "2.154.1" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_cloudwatch.Alarm", + "version": "2.154.1" + } + }, + "ExecutionAbortedAlarm": { + "id": "ExecutionAbortedAlarm", + "path": "facstp-defaults/target/ExecutionAbortedAlarm", + "children": { + "Resource": { + "id": "Resource", + "path": "facstp-defaults/target/ExecutionAbortedAlarm/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::CloudWatch::Alarm", + "aws:cdk:cloudformation:props": { + "alarmDescription": "Alarm for the number of executions that aborted exceeded the threshold of 1. ", + "comparisonOperator": "GreaterThanOrEqualToThreshold", + "dimensions": [ + { + "name": "StateMachineArn", + "value": { + "Ref": "targetStateMachinetestsm59897C57" + } + } + ], + "evaluationPeriods": 1, + "metricName": "ExecutionsAborted", + "namespace": "AWS/States", + "period": 300, + "statistic": "Maximum", + "threshold": 1 + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_cloudwatch.CfnAlarm", + "version": "2.154.1" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_cloudwatch.Alarm", + "version": "2.154.1" } } }, "constructInfo": { "fqn": "@aws-solutions-constructs/aws-constructs-factories.ConstructsFactories", - "version": "2.63.0" + "version": "2.69.0" } }, "lambdatest": { @@ -255,7 +372,7 @@ "path": "facstp-defaults/lambdatest/ServiceRole/ImportServiceRole", "constructInfo": { "fqn": "aws-cdk-lib.Resource", - "version": "2.150.0" + "version": "2.154.1" } }, "Resource": { @@ -294,13 +411,13 @@ }, "constructInfo": { "fqn": "aws-cdk-lib.aws_iam.CfnRole", - "version": "2.150.0" + "version": "2.154.1" } } }, "constructInfo": { "fqn": "aws-cdk-lib.aws_iam.Role", - "version": "2.150.0" + "version": "2.154.1" } }, "Resource": { @@ -324,13 +441,13 @@ }, "constructInfo": { "fqn": "aws-cdk-lib.aws_lambda.CfnFunction", - "version": "2.150.0" + "version": "2.154.1" } } }, "constructInfo": { "fqn": "aws-cdk-lib.aws_lambda.Function", - "version": "2.150.0" + "version": "2.154.1" } }, "tasktest": { @@ -338,7 +455,7 @@ "path": "facstp-defaults/tasktest", "constructInfo": { "fqn": "aws-cdk-lib.aws_stepfunctions_tasks.LambdaInvoke", - "version": "2.150.0" + "version": "2.154.1" } }, "Integ": { @@ -366,7 +483,7 @@ "path": "facstp-defaults/Integ/DefaultTest/DeployAssert/BootstrapVersion", "constructInfo": { "fqn": "aws-cdk-lib.CfnParameter", - "version": "2.150.0" + "version": "2.154.1" } }, "CheckBootstrapVersion": { @@ -374,25 +491,25 @@ "path": "facstp-defaults/Integ/DefaultTest/DeployAssert/CheckBootstrapVersion", "constructInfo": { "fqn": "aws-cdk-lib.CfnRule", - "version": "2.150.0" + "version": "2.154.1" } } }, "constructInfo": { "fqn": "aws-cdk-lib.Stack", - "version": "2.150.0" + "version": "2.154.1" } } }, "constructInfo": { "fqn": "@aws-cdk/integ-tests-alpha.IntegTestCase", - "version": "2.150.0-alpha.0" + "version": "2.154.1-alpha.0" } } }, "constructInfo": { "fqn": "@aws-cdk/integ-tests-alpha.IntegTest", - "version": "2.150.0-alpha.0" + "version": "2.154.1-alpha.0" } }, "BootstrapVersion": { @@ -400,7 +517,7 @@ "path": "facstp-defaults/BootstrapVersion", "constructInfo": { "fqn": "aws-cdk-lib.CfnParameter", - "version": "2.150.0" + "version": "2.154.1" } }, "CheckBootstrapVersion": { @@ -408,13 +525,13 @@ "path": "facstp-defaults/CheckBootstrapVersion", "constructInfo": { "fqn": "aws-cdk-lib.CfnRule", - "version": "2.150.0" + "version": "2.154.1" } } }, "constructInfo": { "fqn": "aws-cdk-lib.Stack", - "version": "2.150.0" + "version": "2.154.1" } }, "Tree": { @@ -428,7 +545,7 @@ }, "constructInfo": { "fqn": "aws-cdk-lib.App", - "version": "2.150.0" + "version": "2.154.1" } } } \ No newline at end of file diff --git a/source/patterns/@aws-solutions-constructs/aws-constructs-factories/test/stepfunctions/state-machine-factory.test.ts b/source/patterns/@aws-solutions-constructs/aws-constructs-factories/test/stepfunctions/state-machine-factory.test.ts index 3f0aa93e3..c9a457e02 100644 --- a/source/patterns/@aws-solutions-constructs/aws-constructs-factories/test/stepfunctions/state-machine-factory.test.ts +++ b/source/patterns/@aws-solutions-constructs/aws-constructs-factories/test/stepfunctions/state-machine-factory.test.ts @@ -78,3 +78,65 @@ test('Existing Log Group', () => { LoggingConfiguration: Match.anyValue() }); }); + +test('check cloudwatch alarm prefix', () => { + const somePrefix = "randomString"; + const stack = new Stack(); + const factories = new ConstructsFactories(stack, 'target'); + const taskOne = new sftasks.EvaluateExpression(stack, 'simpleTask', { + expression: '$.a + $.b' + }); + + const existingLogGroup = new logs.LogGroup(stack, 'existingLogGroup'); + + const startState = sfn.DefinitionBody.fromChainable(taskOne); + + factories.stateMachineFactory('testsm', { + stateMachineProps: { + definitionBody: startState, + logs: { + destination: existingLogGroup + }, + }, + cloudWatchAlarmsPrefix: somePrefix + }); + + const template = Template.fromStack(stack); + const keys = Object.keys((template as any).template.Resources); + const regex = new RegExp(`${somePrefix}Execution`); + const alarms = keys.filter(alarmName => regex.test(alarmName)); + expect(alarms.length).toEqual(3); +}); + +test('Deploy 2 state machines', () => { + const stack = new Stack(); + + const factories = new ConstructsFactories(stack, 'target'); + const taskOne = new sftasks.EvaluateExpression(stack, 'simpleTask', { + expression: '$.a + $.b' + }); + + const startState = sfn.DefinitionBody.fromChainable(taskOne); + + factories.stateMachineFactory('testsm', { + stateMachineProps: { + definitionBody: startState + }, + cloudWatchAlarmsPrefix: "one" + }); + + const taskTwo = new sftasks.EvaluateExpression(stack, 'simpleTaskTwo', { + expression: '$.a + $.b' + }); + + const startStateTwo = sfn.DefinitionBody.fromChainable(taskTwo); + factories.stateMachineFactory('testsm-two', { + stateMachineProps: { + definitionBody: startStateTwo, + }, + cloudWatchAlarmsPrefix: "two" + }); + + Template.fromStack(stack); + // No checks, as our main concern is this has no collisions +}); diff --git a/source/patterns/@aws-solutions-constructs/aws-eventbridge-stepfunctions/lib/index.ts b/source/patterns/@aws-solutions-constructs/aws-eventbridge-stepfunctions/lib/index.ts index 072049f17..eca2b9ac0 100644 --- a/source/patterns/@aws-solutions-constructs/aws-eventbridge-stepfunctions/lib/index.ts +++ b/source/patterns/@aws-solutions-constructs/aws-eventbridge-stepfunctions/lib/index.ts @@ -80,11 +80,16 @@ export class EventbridgeToStepfunctions extends Construct { constructor(scope: Construct, id: string, props: EventbridgeToStepfunctionsProps) { super(scope, id); defaults.CheckEventBridgeProps(props); + defaults.CheckStateMachineProps(props); - const buildStateMachineResponse = defaults.buildStateMachine(this, defaults.idPlaceholder, props.stateMachineProps, - props.logGroupProps); + const buildStateMachineResponse = defaults.buildStateMachine(this, defaults.idPlaceholder, { + stateMachineProps: props.stateMachineProps, + logGroupProps: props.logGroupProps, + createCloudWatchAlarms: props.createCloudWatchAlarms, + }); this.stateMachine = buildStateMachineResponse.stateMachine; this.stateMachineLogGroup = buildStateMachineResponse.logGroup; + this.cloudwatchAlarms = buildStateMachineResponse.cloudWatchAlarms; // Create an IAM role for Events to start the State Machine const eventsRole = new iam.Role(this, 'EventsRuleRole', { @@ -115,10 +120,5 @@ export class EventbridgeToStepfunctions extends Construct { const eventsRuleProps = overrideProps(defaultEventsRuleProps, props.eventRuleProps, true); // Create the Events Rule for the State Machine this.eventsRule = new events.Rule(this, 'EventsRule', eventsRuleProps); - - if (props.createCloudWatchAlarms === undefined || props.createCloudWatchAlarms) { - // Deploy best practices CW Alarms for State Machine - this.cloudwatchAlarms = defaults.buildStepFunctionCWAlarms(this, this.stateMachine); - } } } \ No newline at end of file diff --git a/source/patterns/@aws-solutions-constructs/aws-eventbridge-stepfunctions/test/eventbridge-stepfunctions.test.ts b/source/patterns/@aws-solutions-constructs/aws-eventbridge-stepfunctions/test/eventbridge-stepfunctions.test.ts index a57c4fb24..48656a921 100644 --- a/source/patterns/@aws-solutions-constructs/aws-eventbridge-stepfunctions/test/eventbridge-stepfunctions.test.ts +++ b/source/patterns/@aws-solutions-constructs/aws-eventbridge-stepfunctions/test/eventbridge-stepfunctions.test.ts @@ -187,3 +187,37 @@ test('check custom event bus resource with props when deploy:true', () => { Name: 'testcustomeventbus' }); }); + +test('Deploy 2 constructs', () => { + const stack = new cdk.Stack(); + + new EventbridgeToStepfunctions(stack, 'test-new-eventbridge-stepfunctions', { + stateMachineProps: { + definitionBody: defaults.CreateTestStateMachineDefinitionBody(stack, 'test'), + }, + eventBusProps: { + eventBusName: 'testcustomeventbus' + }, + eventRuleProps: { + eventPattern: { + source: ['solutionsconstructs'] + } + }, + }); + + new EventbridgeToStepfunctions(stack, 'test-second-eventbridge-stepfunctions', { + stateMachineProps: { + definitionBody: defaults.CreateTestStateMachineDefinitionBody(stack, 'seconttest'), + }, + eventBusProps: { + eventBusName: 'secondtestcustomeventbus' + }, + eventRuleProps: { + eventPattern: { + source: ['secondsolutionsconstructs'] + } + }, + }); + Template.fromStack(stack); + // No checks, as our main concern is this has no collisions +}); diff --git a/source/patterns/@aws-solutions-constructs/aws-fargate-stepfunctions/lib/index.ts b/source/patterns/@aws-solutions-constructs/aws-fargate-stepfunctions/lib/index.ts index 7bdc37ab3..fc27e3310 100644 --- a/source/patterns/@aws-solutions-constructs/aws-fargate-stepfunctions/lib/index.ts +++ b/source/patterns/@aws-solutions-constructs/aws-fargate-stepfunctions/lib/index.ts @@ -140,6 +140,7 @@ export class FargateToStepfunctions extends Construct { super(scope, id); defaults.CheckFargateProps(props); defaults.CheckVpcProps(props); + defaults.CheckStateMachineProps(props); this.vpc = defaults.buildVpc(scope, { existingVpc: props.existingVpc, @@ -168,18 +169,17 @@ export class FargateToStepfunctions extends Construct { this.container = createFargateServiceResponse.containerDefinition; } - const buildStateMachineResponse = defaults.buildStateMachine(this, defaults.idPlaceholder, props.stateMachineProps, - props.logGroupProps); + const buildStateMachineResponse = defaults.buildStateMachine(this, defaults.idPlaceholder, { + stateMachineProps: props.stateMachineProps, + logGroupProps: props.logGroupProps, + createCloudWatchAlarms: props.createCloudWatchAlarms, + }); this.stateMachine = buildStateMachineResponse.stateMachine; this.stateMachineLogGroup = buildStateMachineResponse.logGroup; + this.cloudwatchAlarms = buildStateMachineResponse.cloudWatchAlarms; this.stateMachine.grantStartExecution(this.service.taskDefinition.taskRole); - if (props.createCloudWatchAlarms === undefined || props.createCloudWatchAlarms) { - // Deploy best-practice CloudWatch Alarm for state machine - this.cloudwatchAlarms = defaults.buildStepFunctionCWAlarms(this, this.stateMachine); - } - // Add environment variable const stateMachineEnvironmentVariableName = props.stateMachineEnvironmentVariableName || 'STATE_MACHINE_ARN'; this.container.addEnvironment(stateMachineEnvironmentVariableName, this.stateMachine.stateMachineArn); diff --git a/source/patterns/@aws-solutions-constructs/aws-fargate-stepfunctions/test/fargate-stepfunctions.test.ts b/source/patterns/@aws-solutions-constructs/aws-fargate-stepfunctions/test/fargate-stepfunctions.test.ts index ab94057dd..797ba5e83 100644 --- a/source/patterns/@aws-solutions-constructs/aws-fargate-stepfunctions/test/fargate-stepfunctions.test.ts +++ b/source/patterns/@aws-solutions-constructs/aws-fargate-stepfunctions/test/fargate-stepfunctions.test.ts @@ -354,3 +354,36 @@ function testStateMachineProps(stack: cdk.Stack, userProps?: stepfunctions.State return defaults.consolidateProps(defaultTestProp, userProps); } + +test('Deploy 2 constructs', () => { + const stack = new cdk.Stack(); + const existingVpc = defaults.getTestVpc(stack); + + new FargateToStepfunctions(stack, 'test-new-fargate-stepfunctions', { + publicApi: true, + ecrRepositoryArn: defaults.fakeEcrRepoArn, + existingVpc, + clusterProps: { clusterName }, + containerDefinitionProps: { containerName }, + fargateTaskDefinitionProps: { family: familyName }, + fargateServiceProps: { serviceName }, + stateMachineProps: { + definitionBody: defaults.CreateTestStateMachineDefinitionBody(stack, 'test'), + }, + }); + + new FargateToStepfunctions(stack, 'test-second-fargate-stepfunctions', { + publicApi: true, + ecrRepositoryArn: defaults.fakeEcrRepoArn, + existingVpc, + clusterProps: { clusterName }, + containerDefinitionProps: { containerName }, + fargateTaskDefinitionProps: { family: familyName }, + fargateServiceProps: { serviceName }, + stateMachineProps: { + definitionBody: defaults.CreateTestStateMachineDefinitionBody(stack, 'secondtest'), + }, + }); + Template.fromStack(stack); + // No checks, as our main concern is this has no collisions +}); diff --git a/source/patterns/@aws-solutions-constructs/aws-lambda-stepfunctions/lib/index.ts b/source/patterns/@aws-solutions-constructs/aws-lambda-stepfunctions/lib/index.ts index e3379a8ea..0ca52c380 100644 --- a/source/patterns/@aws-solutions-constructs/aws-lambda-stepfunctions/lib/index.ts +++ b/source/patterns/@aws-solutions-constructs/aws-lambda-stepfunctions/lib/index.ts @@ -99,6 +99,7 @@ export class LambdaToStepfunctions extends Construct { super(scope, id); defaults.CheckVpcProps(props); defaults.CheckLambdaProps(props); + defaults.CheckStateMachineProps(props); // Setup vpc if (props.deployVpc || props.existingVpc) { @@ -117,10 +118,14 @@ export class LambdaToStepfunctions extends Construct { } // Setup the state machine - const buildStateMachineResponse = defaults.buildStateMachine(this, defaults.idPlaceholder, props.stateMachineProps, - props.logGroupProps); + const buildStateMachineResponse = defaults.buildStateMachine(this, defaults.idPlaceholder, { + stateMachineProps: props.stateMachineProps, + logGroupProps: props.logGroupProps, + createCloudWatchAlarms: props.createCloudWatchAlarms, + }); this.stateMachine = buildStateMachineResponse.stateMachine; this.stateMachineLogGroup = buildStateMachineResponse.logGroup; + this.cloudwatchAlarms = buildStateMachineResponse.cloudWatchAlarms; // Setup the Lambda function this.lambdaFunction = defaults.buildLambdaFunction(this, { @@ -135,10 +140,5 @@ export class LambdaToStepfunctions extends Construct { // Grant the start execution permission to the Lambda function this.stateMachine.grantStartExecution(this.lambdaFunction); - - if (props.createCloudWatchAlarms === undefined || props.createCloudWatchAlarms) { - // Deploy best-practice CloudWatch Alarm for state machine - this.cloudwatchAlarms = defaults.buildStepFunctionCWAlarms(this, this.stateMachine); - } } } \ No newline at end of file diff --git a/source/patterns/@aws-solutions-constructs/aws-lambda-stepfunctions/test/lambda-stepfunctions.test.ts b/source/patterns/@aws-solutions-constructs/aws-lambda-stepfunctions/test/lambda-stepfunctions.test.ts index 9dd9bf11a..4abf9142e 100644 --- a/source/patterns/@aws-solutions-constructs/aws-lambda-stepfunctions/test/lambda-stepfunctions.test.ts +++ b/source/patterns/@aws-solutions-constructs/aws-lambda-stepfunctions/test/lambda-stepfunctions.test.ts @@ -27,7 +27,7 @@ test('Test deployment with new Lambda function', () => { // Helper declaration new LambdaToStepfunctions(stack, 'lambda-to-step-function-stack', { lambdaFunctionProps: { - runtime: defaults.COMMERCIAL_REGION_LAMBDA_NODE_RUNTIME, + runtime: defaults.COMMERCIAL_REGION_LAMBDA_NODE_RUNTIME, handler: 'index.handler', code: lambda.Code.fromAsset(`${__dirname}/lambda`), environment: { @@ -57,7 +57,7 @@ test('Test deployment with existing Lambda function', () => { const stack = new Stack(); // Helper declaration const lambdaFunctionProps = { - runtime: defaults.COMMERCIAL_REGION_LAMBDA_NODE_RUNTIME, + runtime: defaults.COMMERCIAL_REGION_LAMBDA_NODE_RUNTIME, handler: 'index.handler', code: lambda.Code.fromAsset(`${__dirname}/lambda`), environment: { @@ -88,7 +88,7 @@ test('Test invocation permissions', () => { const stack = new Stack(); // Helper declaration const lambdaFunctionProps = { - runtime: defaults.COMMERCIAL_REGION_LAMBDA_NODE_RUNTIME, + runtime: defaults.COMMERCIAL_REGION_LAMBDA_NODE_RUNTIME, handler: 'index.handler', code: lambda.Code.fromAsset(`${__dirname}/lambda`), environment: { @@ -135,7 +135,7 @@ test('Test the properties', () => { // Helper declaration const pattern = new LambdaToStepfunctions(stack, 'lambda-to-step-function-stack', { lambdaFunctionProps: { - runtime: defaults.COMMERCIAL_REGION_LAMBDA_NODE_RUNTIME, + runtime: defaults.COMMERCIAL_REGION_LAMBDA_NODE_RUNTIME, handler: 'index.handler', code: lambda.Code.fromAsset(`${__dirname}/lambda`), environment: { @@ -164,7 +164,7 @@ test('Test the properties with no CW Alarms', () => { // Helper declaration const pattern = new LambdaToStepfunctions(stack, 'lambda-to-step-function-stack', { lambdaFunctionProps: { - runtime: defaults.COMMERCIAL_REGION_LAMBDA_NODE_RUNTIME, + runtime: defaults.COMMERCIAL_REGION_LAMBDA_NODE_RUNTIME, handler: 'index.handler', code: lambda.Code.fromAsset(`${__dirname}/lambda`), environment: { @@ -192,7 +192,7 @@ test('Test lambda function custom environment variable', () => { // Helper declaration new LambdaToStepfunctions(stack, 'lambda-to-step-function-stack', { lambdaFunctionProps: { - runtime: defaults.COMMERCIAL_REGION_LAMBDA_NODE_RUNTIME, + runtime: defaults.COMMERCIAL_REGION_LAMBDA_NODE_RUNTIME, handler: 'index.handler', code: lambda.Code.fromAsset(`${__dirname}/lambda`) }, @@ -223,7 +223,7 @@ test("Test minimal deployment that deploys a VPC without vpcProps", () => { // Helper declaration new LambdaToStepfunctions(stack, "lambda-to-stepfunctions-stack", { lambdaFunctionProps: { - runtime: defaults.COMMERCIAL_REGION_LAMBDA_NODE_RUNTIME, + runtime: defaults.COMMERCIAL_REGION_LAMBDA_NODE_RUNTIME, handler: 'index.handler', code: lambda.Code.fromAsset(`${__dirname}/lambda`) }, @@ -274,7 +274,7 @@ test("Test minimal deployment that deploys a VPC w/vpcProps", () => { // Helper declaration new LambdaToStepfunctions(stack, "lambda-to-stepfunctions-stack", { lambdaFunctionProps: { - runtime: defaults.COMMERCIAL_REGION_LAMBDA_NODE_RUNTIME, + runtime: defaults.COMMERCIAL_REGION_LAMBDA_NODE_RUNTIME, handler: 'index.handler', code: lambda.Code.fromAsset(`${__dirname}/lambda`) }, @@ -333,7 +333,7 @@ test("Test minimal deployment with an existing VPC", () => { // Helper declaration new LambdaToStepfunctions(stack, "lambda-to-stepfunctions-stack", { lambdaFunctionProps: { - runtime: defaults.COMMERCIAL_REGION_LAMBDA_NODE_RUNTIME, + runtime: defaults.COMMERCIAL_REGION_LAMBDA_NODE_RUNTIME, handler: 'index.handler', code: lambda.Code.fromAsset(`${__dirname}/lambda`) }, @@ -375,7 +375,7 @@ test("Test minimal deployment with an existing VPC and existing Lambda function const stack = new Stack(); const testLambdaFunction = new lambda.Function(stack, 'test-lamba', { - runtime: defaults.COMMERCIAL_REGION_LAMBDA_NODE_RUNTIME, + runtime: defaults.COMMERCIAL_REGION_LAMBDA_NODE_RUNTIME, handler: "index.handler", code: lambda.Code.fromAsset(`${__dirname}/lambda`), }); @@ -409,7 +409,7 @@ test("Confirm CheckVpcProps is called", () => { // Helper declaration new LambdaToStepfunctions(stack, "lambda-to-stepfunctions-stack", { lambdaFunctionProps: { - runtime: defaults.COMMERCIAL_REGION_LAMBDA_NODE_RUNTIME, + runtime: defaults.COMMERCIAL_REGION_LAMBDA_NODE_RUNTIME, handler: 'index.handler', code: lambda.Code.fromAsset(`${__dirname}/lambda`) }, @@ -428,7 +428,7 @@ test('Confirm call to CheckLambdaProps', () => { // Initial Setup const stack = new Stack(); const lambdaFunction = new lambda.Function(stack, 'a-function', { - runtime: defaults.COMMERCIAL_REGION_LAMBDA_NODE_RUNTIME, + runtime: defaults.COMMERCIAL_REGION_LAMBDA_NODE_RUNTIME, handler: 'index.handler', code: lambda.Code.fromAsset(`${__dirname}/lambda`), }); @@ -438,7 +438,7 @@ test('Confirm call to CheckLambdaProps', () => { definitionBody: defaults.CreateTestStateMachineDefinitionBody(stack, 'lamstp-test') }, lambdaFunctionProps: { - runtime: defaults.COMMERCIAL_REGION_LAMBDA_NODE_RUNTIME, + runtime: defaults.COMMERCIAL_REGION_LAMBDA_NODE_RUNTIME, handler: 'index.handler', code: lambda.Code.fromAsset(`${__dirname}/lambda`), }, @@ -456,7 +456,7 @@ test('Test deployment a state machine that needs priveleges for tasks', () => { const stack = new Stack(); const clientFunction = defaults.deployLambdaFunction(stack, { - runtime: defaults.COMMERCIAL_REGION_LAMBDA_NODE_RUNTIME, + runtime: defaults.COMMERCIAL_REGION_LAMBDA_NODE_RUNTIME, handler: 'index.handler', code: lambda.Code.fromAsset(`${__dirname}/lambda`), environment: { @@ -465,7 +465,7 @@ test('Test deployment a state machine that needs priveleges for tasks', () => { }); const taskFunction = defaults.deployLambdaFunction(stack, { - runtime: defaults.COMMERCIAL_REGION_LAMBDA_NODE_RUNTIME, + runtime: defaults.COMMERCIAL_REGION_LAMBDA_NODE_RUNTIME, handler: 'index.handler', code: lambda.Code.fromAsset(`${__dirname}/lambda-task`), environment: { @@ -494,4 +494,32 @@ test('Test deployment a state machine that needs priveleges for tasks', () => { })]) } }); -}); \ No newline at end of file +}); + +test('Deploy 2 constructs', () => { + const stack = new Stack(); + + new LambdaToStepfunctions(stack, 'test-new-eventbridge-stepfunctions', { + lambdaFunctionProps: { + runtime: defaults.COMMERCIAL_REGION_LAMBDA_NODE_RUNTIME, + handler: 'index.handler', + code: lambda.Code.fromAsset(`${__dirname}/lambda`) + }, + stateMachineProps: { + definitionBody: defaults.CreateTestStateMachineDefinitionBody(stack, 'test'), + }, + }); + + new LambdaToStepfunctions(stack, 'test-second-eventbridge-stepfunctions', { + lambdaFunctionProps: { + runtime: defaults.COMMERCIAL_REGION_LAMBDA_NODE_RUNTIME, + handler: 'index.handler', + code: lambda.Code.fromAsset(`${__dirname}/lambda`) + }, + stateMachineProps: { + definitionBody: defaults.CreateTestStateMachineDefinitionBody(stack, 'seconttest'), + }, + }); + Template.fromStack(stack); + // No checks, as our main concern is this has no collisions +}); diff --git a/source/patterns/@aws-solutions-constructs/aws-openapigateway-lambda/lib/openapi-helper.ts b/source/patterns/@aws-solutions-constructs/aws-openapigateway-lambda/lib/openapi-helper.ts index fea66eb67..72b53ffea 100644 --- a/source/patterns/@aws-solutions-constructs/aws-openapigateway-lambda/lib/openapi-helper.ts +++ b/source/patterns/@aws-solutions-constructs/aws-openapigateway-lambda/lib/openapi-helper.ts @@ -163,8 +163,7 @@ export function ObtainApiDefinition(scope: Construct, props: ObtainApiDefinition apiDefinitionWriter.s3Key ); } else if (apiRawInlineSpec) { - const apiInlineSpec = new apigateway.InlineApiDefinition(apiRawInlineSpec); - newApiDefinition = InlineTemplateWriter(apiInlineSpec.bind(scope), apiIntegrationUris); + newApiDefinition = InlineTemplateWriter(apiRawInlineSpec, apiIntegrationUris); } else { throw new Error("No definition provided (this code should be unreachable)"); } @@ -172,8 +171,8 @@ export function ObtainApiDefinition(scope: Construct, props: ObtainApiDefinition return newApiDefinition!; } -function InlineTemplateWriter({ inlineDefinition }: apigateway.ApiDefinitionConfig, templateValues: resources.TemplateValue[]) { - let template = JSON.stringify(inlineDefinition); +function InlineTemplateWriter(rawInlineSpec: any, templateValues: resources.TemplateValue[]) { + let template = JSON.stringify(rawInlineSpec); // This replicates logic in the template writer custom resource (resources/lib/template-writer-custom-resource/index.ts), // any logic changes should be made to both locations every time diff --git a/source/patterns/@aws-solutions-constructs/aws-openapigateway-lambda/test/test.openapigateway-lambda.test.ts b/source/patterns/@aws-solutions-constructs/aws-openapigateway-lambda/test/test.openapigateway-lambda.test.ts index a95392b90..5ec0592bb 100644 --- a/source/patterns/@aws-solutions-constructs/aws-openapigateway-lambda/test/test.openapigateway-lambda.test.ts +++ b/source/patterns/@aws-solutions-constructs/aws-openapigateway-lambda/test/test.openapigateway-lambda.test.ts @@ -625,15 +625,15 @@ test('ObtainApiDefinition from inline JSON spec', () => { tokenToFunctionMap: apiLambdaFunctions }); + const template = Template.fromStack(stack); + template.resourceCountIs("Custom::TemplateWriter", 0); + expect(api).toBeDefined(); expect((api as any).definition).toBeDefined(); expect((api as any).definition.openapi).toEqual("3.0.1"); expect((api as any).definition.info).toBeDefined(); expect((api as any).definition.paths).toBeDefined(); - const template = Template.fromStack(stack); - template.resourceCountIs("Custom::TemplateWriter", 0); - }); test('ObtainApiDefinition from S3 bucket/key', () => { diff --git a/source/patterns/@aws-solutions-constructs/aws-s3-stepfunctions/lib/index.ts b/source/patterns/@aws-solutions-constructs/aws-s3-stepfunctions/lib/index.ts index 83fac8004..cd5514293 100644 --- a/source/patterns/@aws-solutions-constructs/aws-s3-stepfunctions/lib/index.ts +++ b/source/patterns/@aws-solutions-constructs/aws-s3-stepfunctions/lib/index.ts @@ -107,6 +107,7 @@ export class S3ToStepfunctions extends Construct { this.node.setContext("@aws-cdk/aws-s3:serverAccessLogsUseBucketPolicy", true); defaults.CheckS3Props(props); + defaults.CheckStateMachineProps(props); let bucket: s3.IBucket; diff --git a/source/patterns/@aws-solutions-constructs/aws-s3-stepfunctions/test/s3-stepfunctions.test.ts b/source/patterns/@aws-solutions-constructs/aws-s3-stepfunctions/test/s3-stepfunctions.test.ts index a3ac429ab..404a983ec 100644 --- a/source/patterns/@aws-solutions-constructs/aws-s3-stepfunctions/test/s3-stepfunctions.test.ts +++ b/source/patterns/@aws-solutions-constructs/aws-s3-stepfunctions/test/s3-stepfunctions.test.ts @@ -185,3 +185,21 @@ test('s3 bucket with no logging bucket', () => { template.hasResourceProperties("Custom::S3BucketNotifications", {}); expect(construct.s3LoggingBucket).toEqual(undefined); }); + +test('Deploy 2 constructs', () => { + const stack = new cdk.Stack(); + + new S3ToStepfunctions(stack, 'test-new-eventbridge-stepfunctions', { + stateMachineProps: { + definitionBody: defaults.CreateTestStateMachineDefinitionBody(stack, 'test'), + }, + }); + + new S3ToStepfunctions(stack, 'test-second-eventbridge-stepfunctions', { + stateMachineProps: { + definitionBody: defaults.CreateTestStateMachineDefinitionBody(stack, 'seconttest'), + }, + }); + Template.fromStack(stack); + // No checks, as our main concern is this has no collisions +}); diff --git a/source/patterns/@aws-solutions-constructs/core/lib/step-function-helper.ts b/source/patterns/@aws-solutions-constructs/core/lib/step-function-helper.ts index 74d4d862e..fdbd51100 100644 --- a/source/patterns/@aws-solutions-constructs/core/lib/step-function-helper.ts +++ b/source/patterns/@aws-solutions-constructs/core/lib/step-function-helper.ts @@ -31,12 +31,22 @@ import { Construct } from 'constructs'; /* * the id parameter was added to buildStateMachine() long after the original implementation, * this value can be used for the new parameter and ensure behavior is the same. + * (if we just require an id, the state machine name will be changed and it will be a + * destructive change) */ export const idPlaceholder = undefined; +export interface BuildStateMacineProps { + readonly stateMachineProps: sfn.StateMachineProps, + readonly logGroupProps?: logs.LogGroupProps, + readonly createCloudWatchAlarms?: boolean, + readonly cloudWatchAlarmsPrefix?: string +} + export interface BuildStateMachineResponse { readonly stateMachine: sfn.StateMachine, - readonly logGroup: logs.ILogGroup + readonly logGroup: logs.ILogGroup, + readonly cloudWatchAlarms?: cloudwatch.Alarm[] } /** * @internal This is an internal core function and should not be called directly by Solutions Constructs clients. @@ -45,22 +55,21 @@ export interface BuildStateMachineResponse { * @param scope - the construct to which the StateMachine should be attached to. * @param stateMachineProps - user-specified properties to override the default properties. */ -export function buildStateMachine(scope: Construct, id: string | undefined, stateMachineProps: sfn.StateMachineProps, - logGroupProps?: logs.LogGroupProps): BuildStateMachineResponse { +export function buildStateMachine(scope: Construct, id: string | undefined, props: BuildStateMacineProps): BuildStateMachineResponse { let logGroup: logs.ILogGroup; let consolidatedStateMachineProps; // If they sent a logGroup in stateMachineProps - if (stateMachineProps.logs?.destination) { - logGroup = stateMachineProps.logs?.destination; - consolidatedStateMachineProps = stateMachineProps; + if (props.stateMachineProps.logs?.destination) { + logGroup = props.stateMachineProps.logs?.destination; + consolidatedStateMachineProps = props.stateMachineProps; } else { // Three possibilities // 1) logGroupProps not provided - create logGroupProps with just logGroupName // 2) logGroupProps provided with no logGroupName - override logGroupProps.logGroupName // 3) logGroupProps provided with logGroupName - pass unaltered logGroupProps - let consolidatedLogGroupProps = logGroupProps; + let consolidatedLogGroupProps = props.logGroupProps; if (!consolidatedLogGroupProps) { consolidatedLogGroupProps = {}; @@ -84,14 +93,14 @@ export function buildStateMachine(scope: Construct, id: string | undefined, stat logGroup = buildLogGroup(scope, `StateMachineLogGroup${(id ?? '')}`, consolidatedLogGroupProps); // Override the defaults with the user provided props - consolidatedStateMachineProps = overrideProps(smDefaults.DefaultStateMachineProps(logGroup), stateMachineProps); + consolidatedStateMachineProps = overrideProps(smDefaults.DefaultStateMachineProps(logGroup), props.stateMachineProps); } // Override the Cloudwatch permissions to make it more fine grained const newStateMachine = new sfn.StateMachine(scope, `StateMachine${(id ?? '')}`, consolidatedStateMachineProps); // If the client did not pass a role we got the default role and will trim the privileges - if (!stateMachineProps.role) { + if (!props.stateMachineProps.role) { const role = newStateMachine.node.findChild('Role') as iam.Role; const cfnDefaultPolicy = role.node.findChild('DefaultPolicy').node.defaultChild as any; // Override Cfn Nag warning W12: IAM policy should not allow * resource @@ -102,18 +111,26 @@ export function buildStateMachine(scope: Construct, id: string | undefined, stat } ]); } - return { stateMachine: newStateMachine, logGroup }; + const createCloudWatchAlarms: boolean = (props.createCloudWatchAlarms === undefined || props.createCloudWatchAlarms); + const cloudWatchAlarms = createCloudWatchAlarms ? buildStepFunctionCWAlarms(scope, props.cloudWatchAlarmsPrefix, newStateMachine) : undefined; + + return { + stateMachine: newStateMachine, + logGroup, + cloudWatchAlarms + }; } /** * @internal This is an internal core function and should not be called directly by Solutions Constructs clients. */ -export function buildStepFunctionCWAlarms(scope: Construct, sm: sfn.StateMachine): cloudwatch.Alarm[] { +export function buildStepFunctionCWAlarms(scope: Construct, id: string | undefined, sm: sfn.StateMachine): cloudwatch.Alarm[] { // Setup CW Alarms for State Machine const alarms: cloudwatch.Alarm[] = new Array(); + const prefix = id ?? ""; // Sum of number of executions that failed is >= 1 for 5 minutes, 1 consecutive time - alarms.push(new cloudwatch.Alarm(scope, 'ExecutionFailedAlarm', { + alarms.push(new cloudwatch.Alarm(scope, `${prefix}ExecutionFailedAlarm`, { metric: sm.metricFailed({ statistic: 'Sum', period: cdk.Duration.seconds(300), @@ -125,7 +142,7 @@ export function buildStepFunctionCWAlarms(scope: Construct, sm: sfn.StateMachine })); // Sum of number of executions that failed maximum is >= 1 for 5 minute, 1 consecutive time - alarms.push(new cloudwatch.Alarm(scope, 'ExecutionThrottledAlarm', { + alarms.push(new cloudwatch.Alarm(scope, `${prefix}ExecutionThrottledAlarm`, { metric: sm.metricThrottled({ statistic: 'Sum', period: cdk.Duration.seconds(300), @@ -137,7 +154,7 @@ export function buildStepFunctionCWAlarms(scope: Construct, sm: sfn.StateMachine })); // Number of executions that aborted maximum is >= 1 for 5 minute, 1 consecutive time - alarms.push(new cloudwatch.Alarm(scope, 'ExecutionAbortedAlarm', { + alarms.push(new cloudwatch.Alarm(scope, `${prefix}ExecutionAbortedAlarm`, { metric: sm.metricAborted({ statistic: 'Maximum', period: cdk.Duration.seconds(300), @@ -149,4 +166,25 @@ export function buildStepFunctionCWAlarms(scope: Construct, sm: sfn.StateMachine })); return alarms; -} \ No newline at end of file +} + +export interface StateMachineProps { + readonly stateMachineProps: sfn.StateMachineProps; + readonly createCloudWatchAlarms?: boolean; + readonly cloudWatchAlarmsPrefix?: string + readonly logGroupProps?: logs.LogGroupProps; +} + +export function CheckStateMachineProps(propsObject: StateMachineProps | any) { + let errorMessages = ''; + let errorFound = false; + + if ((propsObject.createCloudWatchAlarms === false) && propsObject.cloudWatchAlarmsPrefix) { + errorMessages += 'Error - cloudWatchAlarmsPrefix is invalid when createCloudWatchAlarms is false\n'; + errorFound = true; + } + + if (errorFound) { + throw new Error(errorMessages); + } +} diff --git a/source/patterns/@aws-solutions-constructs/core/test/step-function-helper.test.ts b/source/patterns/@aws-solutions-constructs/core/test/step-function-helper.test.ts index f21e3ae7c..937d0d367 100644 --- a/source/patterns/@aws-solutions-constructs/core/test/step-function-helper.test.ts +++ b/source/patterns/@aws-solutions-constructs/core/test/step-function-helper.test.ts @@ -28,8 +28,10 @@ test('Test deployment w/ custom properties', () => { // Build state machine const buildStateMachineResponse = defaults.buildStateMachine(stack, defaults.idPlaceholder, { - definitionBody: defaults.CreateTestStateMachineDefinitionBody(stack, 'test'), - stateMachineName: 'myStateMachine' + stateMachineProps: { + definitionBody: defaults.CreateTestStateMachineDefinitionBody(stack, 'test'), + stateMachineName: 'myStateMachine' + } }); // Assertion expect(buildStateMachineResponse.stateMachine).toBeDefined(); @@ -50,10 +52,12 @@ test('Test deployment w/ logging enabled', () => { // Build state machine const buildStateMachineResponse = defaults.buildStateMachine(stack, defaults.idPlaceholder, { - definitionBody: defaults.CreateTestStateMachineDefinitionBody(stack, 'test'), - logs: { - destination: logGroup, - level: sfn.LogLevel.FATAL + stateMachineProps: { + definitionBody: defaults.CreateTestStateMachineDefinitionBody(stack, 'test'), + logs: { + destination: logGroup, + level: sfn.LogLevel.FATAL + } } }); // Assertion @@ -84,7 +88,9 @@ test('Check default Cloudwatch permissions', () => { const stack = new Stack(); // Build state machine const buildStateMachineResponse = defaults.buildStateMachine(stack, defaults.idPlaceholder, { - definitionBody: defaults.CreateTestStateMachineDefinitionBody(stack, 'test') + stateMachineProps: { + definitionBody: defaults.CreateTestStateMachineDefinitionBody(stack, 'test') + } }); // Assertion expect(buildStateMachineResponse.stateMachine).toBeDefined(); @@ -137,7 +143,9 @@ test('Check State Machine IAM Policy with 2 Lambda fuctions in State Machine Def const startState = sfn.DefinitionBody.fromChainable(taskOne.next(taskTwo)); // Build state machine const buildStateMachineResponse = defaults.buildStateMachine(stack, defaults.idPlaceholder, { - definitionBody: startState + stateMachineProps: { + definitionBody: startState + } }); // Assertion expect(buildStateMachineResponse.stateMachine).toBeDefined(); @@ -215,7 +223,9 @@ test('Check State Machine IAM Policy with S3 API call in State Machine Definitio // Build state machine const buildStateMachineResponse = defaults.buildStateMachine(stack, defaults.idPlaceholder, { - definitionBody: sfn.DefinitionBody.fromChainable(stateMachineDefinition) + stateMachineProps: { + definitionBody: sfn.DefinitionBody.fromChainable(stateMachineDefinition) + } }); // Assertion expect(buildStateMachineResponse.stateMachine).toBeDefined(); @@ -256,12 +266,52 @@ test('Count State Machine CW Alarms', () => { const stack = new Stack(); // Build state machine const buildStateMachineResponse = defaults.buildStateMachine(stack, defaults.idPlaceholder, { - definitionBody: defaults.CreateTestStateMachineDefinitionBody(stack, 'test') + stateMachineProps: { + definitionBody: defaults.CreateTestStateMachineDefinitionBody(stack, 'test') + } + }); + expect(buildStateMachineResponse.stateMachine).toBeDefined(); + + expect(buildStateMachineResponse.cloudWatchAlarms!.length).toEqual(3); +}); + +test('Confirm CloudWatch Alarm Prefix is used', () => { + const customPrefix = "SomeText"; + // Stack + const stack = new Stack(); + // Build state machine + const buildStateMachineResponse = defaults.buildStateMachine(stack, defaults.idPlaceholder, { + stateMachineProps: { + definitionBody: defaults.CreateTestStateMachineDefinitionBody(stack, 'test') + }, + cloudWatchAlarmsPrefix: customPrefix + }); + expect(buildStateMachineResponse.stateMachine).toBeDefined(); + + expect(buildStateMachineResponse.cloudWatchAlarms!.length).toEqual(3); + // expect() checks look for properties, not the resource ID, so we need to + // exploit knowledge of the internals of template. This may be brittle, + // take care in the future + const template = Template.fromStack(stack); + const keys = Object.keys((template as any).template.Resources); + const regex = new RegExp(`${customPrefix}Execution`); + const alarms = keys.filter(alarmName => regex.test(alarmName)); + expect(alarms.length).toEqual(3); +}); + +test('Skip creating CW alarms', () => { + // Stack + const stack = new Stack(); + // Build state machine + const buildStateMachineResponse = defaults.buildStateMachine(stack, defaults.idPlaceholder, { + stateMachineProps: { + definitionBody: defaults.CreateTestStateMachineDefinitionBody(stack, 'test') + }, + createCloudWatchAlarms: false }); - const cwList = defaults.buildStepFunctionCWAlarms(stack, buildStateMachineResponse.stateMachine); expect(buildStateMachineResponse.stateMachine).toBeDefined(); - expect(cwList.length).toEqual(3); + expect(buildStateMachineResponse.cloudWatchAlarms).not.toBeDefined(); }); test('Test deployment with custom role', () => { @@ -285,8 +335,10 @@ test('Test deployment with custom role', () => { // Build state machine const buildStateMachineResponse = defaults.buildStateMachine(stack, defaults.idPlaceholder, { - definitionBody: defaults.CreateTestStateMachineDefinitionBody(stack, 'test'), - role: customRole + stateMachineProps: { + definitionBody: defaults.CreateTestStateMachineDefinitionBody(stack, 'test'), + role: customRole + } }); // Assertion @@ -306,8 +358,10 @@ test('Confirm format of name', () => { const stack = new Stack(undefined, 'teststack'); // Build state machine const buildStateMachineResponse = defaults.buildStateMachine(stack, defaults.idPlaceholder, { - stateMachineName: 'myStateMachine', - definitionBody: defaults.CreateTestStateMachineDefinitionBody(stack, 'test'), + stateMachineProps: { + stateMachineName: 'myStateMachine', + definitionBody: defaults.CreateTestStateMachineDefinitionBody(stack, 'test'), + } }); // Assertion expect(buildStateMachineResponse.stateMachine).toBeDefined(); @@ -331,7 +385,9 @@ test('Confirm format of name with ID provided', () => { const stack = new Stack(undefined, 'teststack'); // Build state machine const buildStateMachineResponse = defaults.buildStateMachine(stack, 'zxz', { - definitionBody: defaults.CreateTestStateMachineDefinitionBody(stack, 'test'), + stateMachineProps: { + definitionBody: defaults.CreateTestStateMachineDefinitionBody(stack, 'test'), + } }); // Assertion expect(buildStateMachineResponse.stateMachine).toBeDefined(); @@ -354,19 +410,40 @@ test('multiple state machines', () => { const stack = new Stack(undefined, 'teststack'); // Build state machine const buildStateMachineResponse = defaults.buildStateMachine(stack, 'one', { - stateMachineName: 'myStateMachine', - definitionBody: defaults.CreateTestStateMachineDefinitionBody(stack, 'smOne'), + stateMachineProps: { + stateMachineName: 'myStateMachineOne', + definitionBody: defaults.CreateTestStateMachineDefinitionBody(stack, 'smOne'), + }, + cloudWatchAlarmsPrefix: 'one' }); const buildStateMachineResponseTwo = defaults.buildStateMachine(stack, 'two', { - stateMachineName: 'myStateMachine', - definitionBody: defaults.CreateTestStateMachineDefinitionBody(stack, 'smTwo'), + stateMachineProps: { + stateMachineName: 'myStateMachineTwo', + definitionBody: defaults.CreateTestStateMachineDefinitionBody(stack, 'smTwo'), + }, + cloudWatchAlarmsPrefix: 'two' }); const buildStateMachineResponseThree = defaults.buildStateMachine(stack, defaults.idPlaceholder, { - stateMachineName: 'myStateMachine', - definitionBody: defaults.CreateTestStateMachineDefinitionBody(stack, 'smThree'), + stateMachineProps: { + stateMachineName: 'myStateMachineThree', + definitionBody: defaults.CreateTestStateMachineDefinitionBody(stack, 'smThree'), + }, + cloudWatchAlarmsPrefix: 'three' }); // Assertion expect(buildStateMachineResponse.stateMachine).toBeDefined(); expect(buildStateMachineResponseTwo.stateMachine).toBeDefined(); expect(buildStateMachineResponseThree.stateMachine).toBeDefined(); }); + +test('Confirm cloudWatchAlarmsPrefix requires createCloudWatchAlarms', () => { + + const app = () => { + defaults.CheckStateMachineProps({ + createCloudWatchAlarms: false, + cloudWatchAlarmsPrefix: 'prefix' + }); + }; + // Assertion + expect(app).toThrowError('Error - cloudWatchAlarmsPrefix is invalid when createCloudWatchAlarms is false\n'); +}); diff --git a/source/patterns/@aws-solutions-constructs/core/test/utils.test.ts b/source/patterns/@aws-solutions-constructs/core/test/utils.test.ts index f4c54a638..8d7639868 100644 --- a/source/patterns/@aws-solutions-constructs/core/test/utils.test.ts +++ b/source/patterns/@aws-solutions-constructs/core/test/utils.test.ts @@ -338,7 +338,6 @@ test('test addCfnGuardSuppressRules', () => { const template = Template.fromStack(stack); const bucket = template.findResources("AWS::S3::Bucket"); - defaults.printWarning(`DBG*********${JSON.stringify(bucket.testbucketE6E05ABE.Metadata)}`); expect(bucket.testbucketE6E05ABE.Metadata.guard.SuppressedRules[0]).toEqual("ADDED_TO_BUCKET"); expect(bucket.testbucketE6E05ABE.Metadata.guard.SuppressedRules[1]).toEqual("ADDED_TO_CFN_RESOURCE"); }); \ No newline at end of file