From aa28f33772aa92c86093f860606583d0f4642ce0 Mon Sep 17 00:00:00 2001 From: biffgaut <78155736+biffgaut@users.noreply.github.com> Date: Tue, 11 May 2021 13:13:09 -0400 Subject: [PATCH] feat (Add a check for props for all constructs) --- .../aws-lambda-s3/lib/index.ts | 2 + .../aws-lambda-s3/test/lambda-s3.test.ts | 30 +- .../@aws-solutions-constructs/core/index.ts | 1 + .../core/lib/input-validation.ts | 126 ++++++++ .../core/test/input-validation.test.ts | 297 ++++++++++++++++++ 5 files changed, 455 insertions(+), 1 deletion(-) create mode 100644 source/patterns/@aws-solutions-constructs/core/lib/input-validation.ts create mode 100644 source/patterns/@aws-solutions-constructs/core/test/input-validation.test.ts diff --git a/source/patterns/@aws-solutions-constructs/aws-lambda-s3/lib/index.ts b/source/patterns/@aws-solutions-constructs/aws-lambda-s3/lib/index.ts index 8a7efe140..a7ca10ab3 100644 --- a/source/patterns/@aws-solutions-constructs/aws-lambda-s3/lib/index.ts +++ b/source/patterns/@aws-solutions-constructs/aws-lambda-s3/lib/index.ts @@ -94,6 +94,8 @@ export class LambdaToS3 extends Construct { */ constructor(scope: Construct, id: string, props: LambdaToS3Props) { super(scope, id); + defaults.CheckProps(props); + let bucket: s3.IBucket; if (props.existingBucketObj && props.bucketProps) { diff --git a/source/patterns/@aws-solutions-constructs/aws-lambda-s3/test/lambda-s3.test.ts b/source/patterns/@aws-solutions-constructs/aws-lambda-s3/test/lambda-s3.test.ts index 22a19b19d..16301ded8 100644 --- a/source/patterns/@aws-solutions-constructs/aws-lambda-s3/test/lambda-s3.test.ts +++ b/source/patterns/@aws-solutions-constructs/aws-lambda-s3/test/lambda-s3.test.ts @@ -472,4 +472,32 @@ test("Test bad call with existingBucket and bucketProps", () => { }; // Assertion expect(app).toThrowError(); -}); \ No newline at end of file +}); + +test('Test that CheckProps() is flagging errors correctly', () => { + // Stack + const stack = new Stack(); + + const testLambdaFunction = new lambda.Function(stack, 'test-lamba', { + runtime: lambda.Runtime.NODEJS_10_X, + handler: "index.handler", + code: lambda.Code.fromAsset(`${__dirname}/lambda`), + }); + + const app = () => { + new LambdaToS3(stack, "lambda-to-s3-stack", { + existingLambdaObj: testLambdaFunction, + lambdaFunctionProps: { + runtime: lambda.Runtime.NODEJS_10_X, + handler: "index.handler", + code: lambda.Code.fromAsset(`${__dirname}/lambda`), + }, + }); + }; + + // Assertion + expect(app).toThrowError( + "Cannot specify an existing Lambda function AND Lambda function props\n" + ); + +}); diff --git a/source/patterns/@aws-solutions-constructs/core/index.ts b/source/patterns/@aws-solutions-constructs/core/index.ts index fdf246094..986b61ed2 100644 --- a/source/patterns/@aws-solutions-constructs/core/index.ts +++ b/source/patterns/@aws-solutions-constructs/core/index.ts @@ -58,4 +58,5 @@ export * from './lib/glue-table-defaults'; export * from './lib/glue-table-helper'; export * from './lib/glue-database-defaults'; export * from './lib/glue-database-helper'; +export * from './lib/input-validation'; export * from './test/test-helper'; diff --git a/source/patterns/@aws-solutions-constructs/core/lib/input-validation.ts b/source/patterns/@aws-solutions-constructs/core/lib/input-validation.ts new file mode 100644 index 000000000..6643a3f4b --- /dev/null +++ b/source/patterns/@aws-solutions-constructs/core/lib/input-validation.ts @@ -0,0 +1,126 @@ +/** + * Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import * as dynamodb from '@aws-cdk/aws-dynamodb'; +import * as kinesis from '@aws-cdk/aws-kinesis'; +import * as lambda from '@aws-cdk/aws-lambda'; +import * as sqs from '@aws-cdk/aws-sqs'; +import * as mediastore from '@aws-cdk/aws-mediastore'; +import * as s3 from '@aws-cdk/aws-s3'; +import * as sns from '@aws-cdk/aws-sns'; +import * as glue from '@aws-cdk/aws-glue'; +import * as sagemaker from '@aws-cdk/aws-sagemaker'; +import * as secretsmanager from "@aws-cdk/aws-secretsmanager"; + +export interface VerifiedProps { + dynamoTableProps?: dynamodb.TableProps, + existingTableObj?: dynamodb.Table, + + existingStreamObj?: kinesis.Stream; + kinesisStreamProps?: kinesis.StreamProps, + + existingLambdaObj?: lambda.Function, + lambdaFunctionProps?: lambda.FunctionProps, + + existingQueueObj?: sqs.Queue, + queueProps?: sqs.QueueProps, + deployDeadLetterQueue?: boolean, + deadLetterQueueProps?: sqs.QueueProps, + + existingMediaStoreContainerObj?: mediastore.CfnContainer; + mediaStoreContainerProps?: mediastore.CfnContainerProps; + + existingBucketObj?: s3.Bucket, + bucketProps?: s3.BucketProps, + + // topicsProps is an incorrect attribute used in event-rule-sns that + // we need to support + topicProps?: sns.TopicProps, + topicsProps?: sns.TopicProps, + existingTopicObj?: sns.Topic, + + glueJobProps?: glue.CfnJobProps, + existingGlueJob?: glue.CfnJob, + + existingSagemakerEndpointObj?: sagemaker.CfnEndpoint, + endpointProps?: sagemaker.CfnEndpointProps, + + readonly existingSecretObj?: secretsmanager.Secret; + readonly secretProps?: secretsmanager.SecretProps; + +} + +export function CheckProps(propsObject: VerifiedProps | any) { + let errorMessages = ''; + let errorFound = false; + + if (propsObject.dynamoTableProps && propsObject.existingTableObj) { + errorMessages += 'Cannot specify an existing DDB table AND DDB table props\n'; + errorFound = true; + } + + if (propsObject.existingStreamObj && propsObject.kinesisStreamProps) { + errorMessages += 'Cannot specify an existing Stream table AND Stream props\n'; + errorFound = true; + } + + if (propsObject.existingLambdaObj && propsObject.lambdaFunctionProps) { + errorMessages += 'Cannot specify an existing Lambda function AND Lambda function props\n'; + errorFound = true; + } + + if (propsObject.existingQueueObj && propsObject.queueProps) { + errorMessages += 'Cannot specify an existing SQS queue AND SQS queue props\n'; + errorFound = true; + } + + if ((propsObject?.deployDeadLetterQueue == false) && propsObject.deadLetterQueueProps) { + errorMessages += 'Cannot specify no Dead Letter Queue AND Dead Letter Queue props\n'; + errorFound = true; + } + + if (propsObject.existingMediaStoreContainerObj && propsObject.mediaStoreContainerProps) { + errorMessages += 'Cannot specify an existing MediaStore container AND MediaStore container props\n'; + errorFound = true; + } + + if (propsObject.existingBucketObj && propsObject.bucketProps) { + errorMessages += 'Cannot specify an existing S3 bucket AND S3 bucket props\n'; + errorFound = true; + } + + if ((propsObject.topicProps || propsObject.topicsProps) && propsObject.existingTopicObj) { + errorMessages += 'Cannot specify an existing SNS topic AND SNS topic props\n'; + errorFound = true; + } + + if (propsObject.glueJobProps && propsObject.existingGlueJob) { + errorMessages += 'Cannot specify an existing Glue job AND Glue job props\n'; + errorFound = true; + } + + if (propsObject.existingSagemakerEndpointObj && propsObject.endpointProps) { + errorMessages += 'Cannot specify an existing SageMaker endpoint AND SageMaker endpoint props\n'; + errorFound = true; + } + + if (propsObject.existingSecretObj && propsObject.secretProps) { + errorMessages += 'Cannot specify an existing Secret AND Secret props\n'; + errorFound = true; + } + + + if (errorFound) { + throw new Error(errorMessages); + } +} \ No newline at end of file diff --git a/source/patterns/@aws-solutions-constructs/core/test/input-validation.test.ts b/source/patterns/@aws-solutions-constructs/core/test/input-validation.test.ts new file mode 100644 index 000000000..22aaad335 --- /dev/null +++ b/source/patterns/@aws-solutions-constructs/core/test/input-validation.test.ts @@ -0,0 +1,297 @@ +/** + * Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +// Imports +import * as defaults from '../'; +import { Stack } from '@aws-cdk/core'; +import { CreateScrapBucket } from './test-helper'; +import * as lambda from '@aws-cdk/aws-lambda'; +import * as sqs from '@aws-cdk/aws-sqs'; +import { MediaStoreContainerProps } from '../lib/mediastore-defaults'; +import * as mediastore from '@aws-cdk/aws-mediastore'; +import * as kinesis from '@aws-cdk/aws-kinesis'; +import * as sns from '@aws-cdk/aws-sns'; +import * as dynamodb from '@aws-cdk/aws-dynamodb'; +import * as glue from '@aws-cdk/aws-glue'; +import * as iam from '@aws-cdk/aws-iam'; +import { BuildSagemakerEndpoint } from '../lib/sagemaker-helper'; + +test('Test with valid props', () => { + const props: defaults.VerifiedProps = { + }; + + defaults.CheckProps(props); +}); + +test('Test fail DynamoDB table check', () => { + const stack = new Stack(); + + const props: defaults.VerifiedProps = { + existingTableObj: new dynamodb.Table(stack, 'placeholder', defaults.DefaultTableProps), + dynamoTableProps: defaults.DefaultTableProps, + }; + + const app = () => { + defaults.CheckProps(props); + }; + + // Assertion + expect(app).toThrowError('Cannot specify an existing DDB table AND DDB table props\n'); +}); + +test('Test fail Lambda function check', () => { + const stack = new Stack(); + + const props: defaults.VerifiedProps = { + lambdaFunctionProps: { + code: lambda.Code.fromAsset(`${__dirname}/lambda`), + runtime: lambda.Runtime.NODEJS_14_X, + handler: 'index.handler', + }, + existingLambdaObj: new lambda.Function(stack, 'placeholder', { + code: lambda.Code.fromAsset(`${__dirname}/lambda`), + runtime: lambda.Runtime.NODEJS_14_X, + handler: 'index.handler', + }), + }; + + const app = () => { + defaults.CheckProps(props); + }; + + // Assertion + expect(app).toThrowError('Cannot specify an existing Lambda function AND Lambda function props\n'); +}); + +test("Test fail SQS Queue check", () => { + const stack = new Stack(); + + const props: defaults.VerifiedProps = { + queueProps: {}, + existingQueueObj: new sqs.Queue(stack, 'placeholder', {}), + }; + + const app = () => { + defaults.CheckProps(props); + }; + + // Assertion + expect(app).toThrowError('Cannot specify an existing SQS queue AND SQS queue props\n'); +}); + +test('Test fail Dead Letter Queue check', () => { + + const props: defaults.VerifiedProps = { + deployDeadLetterQueue: false, + deadLetterQueueProps: {}, + }; + + const app = () => { + defaults.CheckProps(props); + }; + + // Assertion + expect(app).toThrowError('Cannot specify no Dead Letter Queue AND Dead Letter Queue props\n'); +}); + +test('Test fail Dead Letter Queue check with no deployDeadLetterQueue flag', () => { + + const props: defaults.VerifiedProps = { + deadLetterQueueProps: {}, + }; + + // Should not flag an error + defaults.CheckProps(props); + +}); + +test('Test fail MediaStore container check', () => { + const stack = new Stack(); + + const mediaStoreContainer = new mediastore.CfnContainer(stack, 'placeholder', + MediaStoreContainerProps()); + + const props: defaults.VerifiedProps = { + mediaStoreContainerProps: MediaStoreContainerProps(), + existingMediaStoreContainerObj: mediaStoreContainer, + }; + + const app = () => { + defaults.CheckProps(props); + }; + + // Assertion + expect(app).toThrowError('Cannot specify an existing MediaStore container AND MediaStore container props\n'); +}); + +test('Test fail Kinesis stream check', () => { + const stack = new Stack(); + + const stream = new kinesis.Stream(stack, 'placeholder', { + + }); + + const props: defaults.VerifiedProps = { + existingStreamObj: stream, + kinesisStreamProps: {} + }; + + const app = () => { + defaults.CheckProps(props); + }; + + // Assertion + expect(app).toThrowError('Cannot specify an existing Stream table AND Stream props\n'); +}); + +test('Test fail S3 check', () => { + const stack = new Stack(); + + const props: defaults.VerifiedProps = { + existingBucketObj: CreateScrapBucket(stack, { }), + bucketProps: {}, + }; + + const app = () => { + defaults.CheckProps(props); + }; + + // Assertion + expect(app).toThrowError('Cannot specify an existing S3 bucket AND S3 bucket props\n'); +}); + +test('Test fail SNS topic check', () => { + const stack = new Stack(); + + const props: defaults.VerifiedProps = { + topicProps: {}, + existingTopicObj: new sns.Topic(stack, 'placeholder', {}) + }; + + const app = () => { + defaults.CheckProps(props); + }; + + // Assertion + expect(app).toThrowError('Cannot specify an existing SNS topic AND SNS topic props\n'); +}); + +test('Test fail SNS topic check with bad topic attribute name', () => { + const stack = new Stack(); + + const props: defaults.VerifiedProps = { + topicsProps: {}, + existingTopicObj: new sns.Topic(stack, 'placeholder', {}) + }; + + const app = () => { + defaults.CheckProps(props); + }; + + // Assertion + expect(app).toThrowError('Cannot specify an existing SNS topic AND SNS topic props\n'); +}); + +test('Test fail Glue job check', () => { + const stack = new Stack(); + + const _jobRole = new iam.Role(stack, 'CustomETLJobRole', { + assumedBy: new iam.ServicePrincipal('glue.amazonaws.com') + }); + + const jobProps: glue.CfnJobProps = defaults.DefaultGlueJobProps(_jobRole, { + name: 'placeholder', + pythonVersion: '3', + scriptLocation: 's3://fakelocation/script' + }, 'testETLJob', {}, '1.0'); + + const job = new glue.CfnJob(stack, 'placeholder', jobProps); + + const props: defaults.VerifiedProps = { + glueJobProps: jobProps, + existingGlueJob: job + }; + + const app = () => { + defaults.CheckProps(props); + }; + + // Assertion + expect(app).toThrowError('Cannot specify an existing Glue job AND Glue job props\n'); +}); + +test('Test fail SageMaker endpoint check', () => { + const stack = new Stack(); + + // Build Sagemaker Inference Endpoint + const modelProps = { + primaryContainer: { + image: ".dkr.ecr..amazonaws.com/linear-learner:latest", + modelDataUrl: "s3:////model.tar.gz", + }, + }; + + const [endpoint] = BuildSagemakerEndpoint(stack, { modelProps }); + + const props: defaults.VerifiedProps = { + existingSagemakerEndpointObj: endpoint, + endpointProps: { + endpointConfigName: 'placeholder' + } + }; + + const app = () => { + defaults.CheckProps(props); + }; + + // Assertion + expect(app).toThrowError('Cannot specify an existing SageMaker endpoint AND SageMaker endpoint props\n'); +}); + +test('Test fail Secret check', () => { + const stack = new Stack(); + + const props: defaults.VerifiedProps = { + secretProps: {}, + existingSecretObj: defaults.buildSecretsManagerSecret(stack, 'secret', {}), + }; + + const app = () => { + defaults.CheckProps(props); + }; + + // Assertion + expect(app).toThrowError('Cannot specify an existing Secret AND Secret props\n'); +}); + +test('Test fail multiple failures message', () => { + const stack = new Stack(); + + const props: defaults.VerifiedProps = { + secretProps: {}, + existingSecretObj: defaults.buildSecretsManagerSecret(stack, 'secret', {}), + topicProps: {}, + existingTopicObj: new sns.Topic(stack, 'placeholder', {}) + }; + + const app = () => { + defaults.CheckProps(props); + }; + + // Assertion + expect(app).toThrowError( + 'Cannot specify an existing SNS topic AND SNS topic props\n' + + 'Cannot specify an existing Secret AND Secret props\n' + ); +}); +