From 3c0f5e03490ffd8d554346dd84ca3e9f60ddc160 Mon Sep 17 00:00:00 2001 From: Dmitry Balabanov Date: Fri, 29 Jul 2022 17:34:52 +0200 Subject: [PATCH] Add JavaScript version --- javascript/.gitignore | 5 + javascript/.npmignore | 3 + javascript/README.md | 67 ++++ javascript/api/infrastructure.js | 81 ++++ javascript/api/runtime/get-save-image.js | 51 +++ javascript/app.js | 28 ++ javascript/cdk.json | 35 ++ javascript/integration/infrastructure.js | 50 +++ .../integration/runtime/package-lock.json | 49 +++ javascript/integration/runtime/package.json | 13 + javascript/integration/runtime/send-email.js | 39 ++ javascript/package-lock.json | 358 ++++++++++++++++++ javascript/package.json | 18 + javascript/recognition/infrastructure.js | 110 ++++++ .../recognition/runtime/image-recognition.js | 101 +++++ javascript/recognition/runtime/list-images.js | 20 + 16 files changed, 1028 insertions(+) create mode 100644 javascript/.gitignore create mode 100644 javascript/.npmignore create mode 100644 javascript/README.md create mode 100644 javascript/api/infrastructure.js create mode 100644 javascript/api/runtime/get-save-image.js create mode 100644 javascript/app.js create mode 100644 javascript/cdk.json create mode 100644 javascript/integration/infrastructure.js create mode 100644 javascript/integration/runtime/package-lock.json create mode 100644 javascript/integration/runtime/package.json create mode 100644 javascript/integration/runtime/send-email.js create mode 100644 javascript/package-lock.json create mode 100644 javascript/package.json create mode 100644 javascript/recognition/infrastructure.js create mode 100644 javascript/recognition/runtime/image-recognition.js create mode 100644 javascript/recognition/runtime/list-images.js diff --git a/javascript/.gitignore b/javascript/.gitignore new file mode 100644 index 0000000..21dc762 --- /dev/null +++ b/javascript/.gitignore @@ -0,0 +1,5 @@ +node_modules + +# CDK asset staging directory +.cdk.staging +cdk.out diff --git a/javascript/.npmignore b/javascript/.npmignore new file mode 100644 index 0000000..5de422a --- /dev/null +++ b/javascript/.npmignore @@ -0,0 +1,3 @@ +# CDK asset staging directory +.cdk.staging +cdk.out diff --git a/javascript/README.md b/javascript/README.md new file mode 100644 index 0000000..96f81c7 --- /dev/null +++ b/javascript/README.md @@ -0,0 +1,67 @@ +## Amazon CodeWhisperer Example + +This repository accompanies a hands-on workshop that demonstrates how to leverage Amazon CodeWhisperer for building a fully fledged serverless app on AWS. + +## Amazon CodeWhisperer + +Amazon CodeWhisperer is a machine learning (ML)–powered service that helps improve developer productivity by generating code recommendations based on their comments in natural language and code in the integrated development environment (IDE). + +With Amazon CodeWhisperer, developers can simply write a comment that outlines a specific task in plain English, such as “upload a file to S3.” Based on this, CodeWhisperer automatically determines which cloud services and public libraries are best suited for the specified task, builds the specific code on the fly, and recommends the generated code snippets directly in the IDE. Moreover, CodeWhisperer seamlessly integrates with your Visual Studio Code and JetBrains IDEs, in such a way that you can stay focused and never leave the development environment. See the [Amazon CodeWhisperer](https://aws.amazon.com/codewhisperer/) page for details. + +## Try out Amazon CodeWhisperer + +You can use this code repository to try out Amazon CodeWhisperer by building a full-fledged, event-driven, serverless application. With the aid of Amazon CodeWhisperer, you'll write your own code that runs on top of AWS Lambda to interact with Amazon DynamoDB, Amazon SNS, Amazon SQS, Amazon S3, and third-party HTTP APIs to perform image recognition using Amazon Rekognition. The users can interact with the application by sending the URL of an image for processing, or by listing the images and the objects present on each image. + +### Architecture + +![architecture](images/architecture.png) + +### Prerequisites + +To use the CodeWhisperer with this repo, you will need an AWS account and an active Amazon CodeWhisperer activation. + +### Setup + +Use the workshop description and follow the steps for building and deploying the application. + +## Getting Help + +Use the community resources below for getting help with AWS CodeGuru Reviewer. + +- Use GitHub issues to report bugs and request features. +- Open a support ticket with [AWS Support](https://docs.aws.amazon.com/awssupport/latest/user/getting-started.html). +- For contributing guidelines, refer to [CONTRIBUTING](CONTRIBUTING.md). + +## Contributing + +See [CONTRIBUTING](CONTRIBUTING.md#security-issue-notifications) for more information. + +## License + +This project is licensed under the MIT-0 License. See the [LICENSE](LICENSE) file. + +## Working with this CDK JavaScript project + +The `cdk.json` file tells the CDK Toolkit how to execute your app. This project is set up like a standard NodeJS project. + +You can install the required dependencies: + +```bash +npm install +``` + +At this point you can now synthesize the CloudFormation template for this code. + +```bash +cdk synth +``` + +## Useful commands + +- `cdk ls` list all stacks in the app +- `cdk synth` emits the synthesized CloudFormation template +- `cdk deploy` deploy this stack to your default AWS account/region +- `cdk diff` compare deployed stack with current state +- `cdk docs` open CDK documentation + +Enjoy! diff --git a/javascript/api/infrastructure.js b/javascript/api/infrastructure.js new file mode 100644 index 0000000..f56e208 --- /dev/null +++ b/javascript/api/infrastructure.js @@ -0,0 +1,81 @@ +const { Stack, Duration } = require('aws-cdk-lib'); +const s3 = require('aws-cdk-lib/aws-s3'); +const lambda = require('aws-cdk-lib/aws-lambda'); +const apigateway = require('aws-cdk-lib/aws-apigateway'); +const sqs = require('aws-cdk-lib/aws-sqs'); +const sns = require('aws-cdk-lib/aws-sns'); +const sns_subs = require('aws-cdk-lib/aws-sns-subscriptions'); +const s3n = require('aws-cdk-lib/aws-s3-notifications'); + +class APIStack extends Stack { + + get sqsUrl() { return this.uploadQueueUrl } + + get sqsArn() { return this.uploadQueueArn } + + constructor(scope, id, props) { + super(scope, id, props); + + const bucket = new s3.Bucket(this, "CW-Workshop-Images") + + const imageGetAndSaveLambda = new lambda.Function( + this, + "ImageGetAndSaveLambda", { + functionName: "ImageGetAndSaveLambda", + runtime: lambda.Runtime.NODEJS_16_X, + code: lambda.Code.fromAsset("api/runtime"), + handler: "get-save-image.handler", + environment: { "BUCKET_NAME": bucket.bucketName } + } + ) + + bucket.grantReadWrite(imageGetAndSaveLambda) + + const api = new apigateway.RestApi( + this, + "REST_API", { + restApiName: "Image Upload Service", + description: "CW workshop - upload image for workshop." + } + ) + + const getImageIntegration = new apigateway.LambdaIntegration( + imageGetAndSaveLambda, { + requestTemplates: { "application/json": '{ "statusCode": "200" }' } + } + ) + + api.root.addMethod("GET", getImageIntegration) + + const uploadQueue = new sqs.Queue( + this, + "uploaded_image_queue", { + visibilityTimeout: Duration.seconds(30) + } + ) + + this.uploadQueueUrl = uploadQueue.queueUrl + this.uploadQueueArn = uploadQueue.queueArn + + const sqsSubscription = new sns_subs.SqsSubscription( + uploadQueue, { + rawMessageDelivery: true + } + ) + + const uploadEventTopic = new sns.Topic( + this, + "uploaded_image_topic" + ) + + uploadEventTopic.addSubscription(sqsSubscription) + + bucket.addEventNotification( + s3.EventType.OBJECT_CREATED_PUT, + new s3n.SnsDestination(uploadEventTopic) + ) + + } +} + +module.exports = { APIStack } \ No newline at end of file diff --git a/javascript/api/runtime/get-save-image.js b/javascript/api/runtime/get-save-image.js new file mode 100644 index 0000000..24ab61c --- /dev/null +++ b/javascript/api/runtime/get-save-image.js @@ -0,0 +1,51 @@ +const fs = require('fs'); +const https = require('https'); +const AWS = require('aws-sdk') +const s3 = new AWS.S3() + +// 1.) Function to download a file from URL +const downloadFileFromUrl = async (url, fileName) => { + return new Promise((resolve, reject) => { + fileName = `/tmp/${fileName}`; + const file = fs.createWriteStream(fileName); + https.get(url, response => { + response.pipe(file); + file.on('finish', () => { + file.close(() => resolve()); + }); + }).on('error', error => { + fs.unlink(fileName); + reject(error.message); + }); + }); +} + +// 2.) Function to upload image to S3 +const uploadImage = async (bucket, fileName) => { + const inputStream = fs.createReadStream(`/tmp/${fileName}`); + + const params = { + Bucket: bucket, + Key: fileName, + Body: inputStream + } + + const s3Response = await s3.upload(params).promise() + return s3Response +} + +exports.handler = async function (event, context) { + const S3_BUCKET = process.env.BUCKET_NAME + + const url = event.queryStringParameters.url + const name = event.queryStringParameters.name + + // pass the output of method #1 as input to method #2 + await downloadFileFromUrl(url, name) + await uploadImage(S3_BUCKET, name) + + return { + statusCode: 200, + body: "Successfully Uploaded Img!" + } +} diff --git a/javascript/app.js b/javascript/app.js new file mode 100644 index 0000000..d0cb890 --- /dev/null +++ b/javascript/app.js @@ -0,0 +1,28 @@ +#!/usr/bin/env node + +const cdk = require('aws-cdk-lib'); +const { APIStack } = require('./api/infrastructure'); +const { IntegrationStack } = require('./integration/infrastructure'); +const { RekognitionStack } = require('./recognition/infrastructure'); + +const DEFAULT_REGION = 'us-east-2' + +const defaultEnvironment = { + region: DEFAULT_REGION +} + +const app = new cdk.App(); + +const apiStack = new APIStack(app, 'APIStack', { env: defaultEnvironment }); +const integrationStack = new IntegrationStack(app, "IntegrationStack", { env: defaultEnvironment }) +new RekognitionStack( + app, + "RekognitionStack", { + sqsUrl: apiStack.sqsUrl, + sqsArn: apiStack.sqsArn, + snsArn: integrationStack.snsArn, + env: defaultEnvironment +} +) + +app.synth() diff --git a/javascript/cdk.json b/javascript/cdk.json new file mode 100644 index 0000000..ddcdce8 --- /dev/null +++ b/javascript/cdk.json @@ -0,0 +1,35 @@ +{ + "app": "node app.js", + "watch": { + "include": [ + "**" + ], + "exclude": [ + "README.md", + "cdk*.json", + "package*.json", + "yarn.lock", + "node_modules" + ] + }, + "context": { + "@aws-cdk/aws-apigateway:usagePlanKeyOrderInsensitiveId": true, + "@aws-cdk/core:stackRelativeExports": true, + "@aws-cdk/aws-rds:lowercaseDbIdentifier": true, + "@aws-cdk/aws-lambda:recognizeVersionProps": true, + "@aws-cdk/aws-lambda:recognizeLayerVersion": true, + "@aws-cdk/aws-cloudfront:defaultSecurityPolicyTLSv1.2_2021": true, + "@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true, + "@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": true, + "@aws-cdk/core:checkSecretUsage": true, + "@aws-cdk/aws-iam:minimizePolicies": true, + "@aws-cdk/core:validateSnapshotRemovalPolicy": true, + "@aws-cdk/aws-codepipeline:crossAccountKeyAliasStackSafeResourceName": true, + "@aws-cdk/aws-s3:createDefaultLoggingPolicy": true, + "@aws-cdk/aws-sns-subscriptions:restrictSqsDescryption": true, + "@aws-cdk/core:target-partitions": [ + "aws", + "aws-cn" + ] + } +} diff --git a/javascript/integration/infrastructure.js b/javascript/integration/infrastructure.js new file mode 100644 index 0000000..a99739e --- /dev/null +++ b/javascript/integration/infrastructure.js @@ -0,0 +1,50 @@ +const { Stack, Duration } = require('aws-cdk-lib'); +const lambda = require('aws-cdk-lib/aws-lambda'); +const lambda_events = require('aws-cdk-lib/aws-lambda-event-sources'); +const sqs = require('aws-cdk-lib/aws-sqs'); +const sns = require('aws-cdk-lib/aws-sns'); +const sns_subs = require('aws-cdk-lib/aws-sns-subscriptions'); + +class IntegrationStack extends Stack { + + get snsArn() { return this.rekognizedEventTopicArn } + + constructor(scope, id, props) { + super(scope, id, props); + + const rekognizedQueue = new sqs.Queue( + this, + "rekognized_image_queue", { + visibilityTimeout: Duration.seconds(30) + } + ) + + const sqsSubscription = new sns_subs.SqsSubscription( + rekognizedQueue, { + rawMessageDelivery: true + } + ) + + const rekognizedEventTopic = new sns.Topic( + this, + "rekognized_image_topic" + ) + + this.rekognizedEventTopicArn = rekognizedEventTopic.topicArn + rekognizedEventTopic.addSubscription(sqsSubscription) + + const integrationLambda = new lambda.Function( + this, + "IntegrationLambda", { + runtime: lambda.Runtime.NODEJS_16_X, + handler: "send-email.handler", + code: lambda.Code.fromAsset("integration/runtime") + } + ) + + const invokeEventSource = new lambda_events.SqsEventSource(rekognizedQueue) + integrationLambda.addEventSource(invokeEventSource) + } +} + +module.exports = { IntegrationStack } \ No newline at end of file diff --git a/javascript/integration/runtime/package-lock.json b/javascript/integration/runtime/package-lock.json new file mode 100644 index 0000000..188aa77 --- /dev/null +++ b/javascript/integration/runtime/package-lock.json @@ -0,0 +1,49 @@ +{ + "name": "send-email", + "version": "0.1.0", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "send-email", + "version": "0.1.0", + "dependencies": { + "xml-js": "1.6.11" + }, + "bin": { + "send-mail": "send-email.js" + } + }, + "node_modules/sax": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", + "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==" + }, + "node_modules/xml-js": { + "version": "1.6.11", + "resolved": "https://registry.npmjs.org/xml-js/-/xml-js-1.6.11.tgz", + "integrity": "sha512-7rVi2KMfwfWFl+GpPg6m80IVMWXLRjO+PxTq7V2CDhoGak0wzYzFgUY2m4XJ47OGdXd8eLE8EmwfAmdjw7lC1g==", + "dependencies": { + "sax": "^1.2.4" + }, + "bin": { + "xml-js": "bin/cli.js" + } + } + }, + "dependencies": { + "sax": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", + "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==" + }, + "xml-js": { + "version": "1.6.11", + "resolved": "https://registry.npmjs.org/xml-js/-/xml-js-1.6.11.tgz", + "integrity": "sha512-7rVi2KMfwfWFl+GpPg6m80IVMWXLRjO+PxTq7V2CDhoGak0wzYzFgUY2m4XJ47OGdXd8eLE8EmwfAmdjw7lC1g==", + "requires": { + "sax": "^1.2.4" + } + } + } +} diff --git a/javascript/integration/runtime/package.json b/javascript/integration/runtime/package.json new file mode 100644 index 0000000..41775f5 --- /dev/null +++ b/javascript/integration/runtime/package.json @@ -0,0 +1,13 @@ +{ + "name": "send-email", + "version": "0.1.0", + "bin": { + "send-mail": "send-email.js" + }, + "scripts": { + "build": "echo \"The build step is not required when using JavaScript!\" && exit 0" + }, + "dependencies": { + "xml-js":"1.6.11" + } +} diff --git a/javascript/integration/runtime/send-email.js b/javascript/integration/runtime/send-email.js new file mode 100644 index 0000000..d71d27d --- /dev/null +++ b/javascript/integration/runtime/send-email.js @@ -0,0 +1,39 @@ +var convert = require('xml-js') + +// 1.) Convert JSON string to XML string +const jsonToXml = function (json) { + return convert.json2xml(json, { compact: true, spaces: 4 }) +} + +// 2.) Send string with HTTP POST +const post = async (url, data) => { + return new Promise((resolve, reject) => { + const xhr = new XMLHttpRequest() + xhr.open('POST', url) + xhr.setRequestHeader('Content-Type', 'application/xml') + xhr.onload = () => { + if (xhr.status >= 200 && xhr.status < 300) { + resolve(xhr.response) + } else { + reject(xhr.statusText) + } + } + xhr.onerror = () => reject(xhr.statusText) + xhr.send(data) + }) +} + +exports.handler = async function (event, context) { + + // call method 1.) with var "event" to convert json to xml + const data = jsonToXml(event) + console.log(data) + + // call method 2.) to post xml + // await post('https://www.example.com/sendmail', xml) + + return { + statusCode: 200, + message: "Success!" + } +} diff --git a/javascript/package-lock.json b/javascript/package-lock.json new file mode 100644 index 0000000..80422ae --- /dev/null +++ b/javascript/package-lock.json @@ -0,0 +1,358 @@ +{ + "name": "app", + "version": "0.1.0", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "app", + "version": "0.1.0", + "dependencies": { + "aws-cdk-lib": "2.33.0", + "constructs": "^10.0.0" + }, + "bin": { + "app": "app.js" + }, + "devDependencies": { + "aws-cdk": "2.33.0" + } + }, + "node_modules/aws-cdk": { + "version": "2.33.0", + "resolved": "https://registry.npmjs.org/aws-cdk/-/aws-cdk-2.33.0.tgz", + "integrity": "sha512-pwqqXzdkyBraKARdyZ6OqMIbEHlnM0x5LJ08zFYQGcrC+jk/e3Y3XWGJrapC6sAKNwQNF78dcJbTSNBjjLXMdQ==", + "dev": true, + "bin": { + "cdk": "bin/cdk" + }, + "engines": { + "node": ">= 14.15.0" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/aws-cdk-lib": { + "version": "2.33.0", + "resolved": "https://registry.npmjs.org/aws-cdk-lib/-/aws-cdk-lib-2.33.0.tgz", + "integrity": "sha512-+aV6+P3RROFndkw9/mtXCciL1RL2tHssju6kgwmml0XIqcnjJ8qyCR23fE8MEq49Q+6dQ8sBN2HtrdKHw/sgnw==", + "bundleDependencies": [ + "@balena/dockerignore", + "case", + "fs-extra", + "ignore", + "jsonschema", + "minimatch", + "punycode", + "semver", + "yaml" + ], + "dependencies": { + "@balena/dockerignore": "^1.0.2", + "case": "1.6.3", + "fs-extra": "^9.1.0", + "ignore": "^5.2.0", + "jsonschema": "^1.4.1", + "minimatch": "^3.1.2", + "punycode": "^2.1.1", + "semver": "^7.3.7", + "yaml": "1.10.2" + }, + "engines": { + "node": ">= 14.15.0" + }, + "peerDependencies": { + "constructs": "^10.0.0" + } + }, + "node_modules/aws-cdk-lib/node_modules/@balena/dockerignore": { + "version": "1.0.2", + "inBundle": true, + "license": "Apache-2.0" + }, + "node_modules/aws-cdk-lib/node_modules/at-least-node": { + "version": "1.0.0", + "inBundle": true, + "license": "ISC", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/aws-cdk-lib/node_modules/balanced-match": { + "version": "1.0.2", + "inBundle": true, + "license": "MIT" + }, + "node_modules/aws-cdk-lib/node_modules/brace-expansion": { + "version": "1.1.11", + "inBundle": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/aws-cdk-lib/node_modules/case": { + "version": "1.6.3", + "inBundle": true, + "license": "(MIT OR GPL-3.0-or-later)", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/aws-cdk-lib/node_modules/concat-map": { + "version": "0.0.1", + "inBundle": true, + "license": "MIT" + }, + "node_modules/aws-cdk-lib/node_modules/fs-extra": { + "version": "9.1.0", + "inBundle": true, + "license": "MIT", + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/aws-cdk-lib/node_modules/graceful-fs": { + "version": "4.2.10", + "inBundle": true, + "license": "ISC" + }, + "node_modules/aws-cdk-lib/node_modules/ignore": { + "version": "5.2.0", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/aws-cdk-lib/node_modules/jsonfile": { + "version": "6.1.0", + "inBundle": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/aws-cdk-lib/node_modules/jsonschema": { + "version": "1.4.1", + "inBundle": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/aws-cdk-lib/node_modules/lru-cache": { + "version": "6.0.0", + "inBundle": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/aws-cdk-lib/node_modules/minimatch": { + "version": "3.1.2", + "inBundle": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/aws-cdk-lib/node_modules/punycode": { + "version": "2.1.1", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/aws-cdk-lib/node_modules/semver": { + "version": "7.3.7", + "inBundle": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/aws-cdk-lib/node_modules/universalify": { + "version": "2.0.0", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/aws-cdk-lib/node_modules/yallist": { + "version": "4.0.0", + "inBundle": true, + "license": "ISC" + }, + "node_modules/aws-cdk-lib/node_modules/yaml": { + "version": "1.10.2", + "inBundle": true, + "license": "ISC", + "engines": { + "node": ">= 6" + } + }, + "node_modules/constructs": { + "version": "10.1.60", + "resolved": "https://registry.npmjs.org/constructs/-/constructs-10.1.60.tgz", + "integrity": "sha512-MI4MfAQu8M/Nk8/60/phguJXriYjfzpNr/Iww4EAfAogkW/5Td3Xb6B437kBG7ISFSiDzJ7v49uFhIsoAQB9nQ==", + "engines": { + "node": ">= 14.17.0" + } + } + }, + "dependencies": { + "aws-cdk": { + "version": "2.33.0", + "resolved": "https://registry.npmjs.org/aws-cdk/-/aws-cdk-2.33.0.tgz", + "integrity": "sha512-pwqqXzdkyBraKARdyZ6OqMIbEHlnM0x5LJ08zFYQGcrC+jk/e3Y3XWGJrapC6sAKNwQNF78dcJbTSNBjjLXMdQ==", + "dev": true, + "requires": { + "fsevents": "2.3.2" + } + }, + "aws-cdk-lib": { + "version": "2.33.0", + "resolved": "https://registry.npmjs.org/aws-cdk-lib/-/aws-cdk-lib-2.33.0.tgz", + "integrity": "sha512-+aV6+P3RROFndkw9/mtXCciL1RL2tHssju6kgwmml0XIqcnjJ8qyCR23fE8MEq49Q+6dQ8sBN2HtrdKHw/sgnw==", + "requires": { + "@balena/dockerignore": "^1.0.2", + "case": "1.6.3", + "fs-extra": "^9.1.0", + "ignore": "^5.2.0", + "jsonschema": "^1.4.1", + "minimatch": "^3.1.2", + "punycode": "^2.1.1", + "semver": "^7.3.7", + "yaml": "1.10.2" + }, + "dependencies": { + "@balena/dockerignore": { + "version": "1.0.2", + "bundled": true + }, + "at-least-node": { + "version": "1.0.0", + "bundled": true + }, + "balanced-match": { + "version": "1.0.2", + "bundled": true + }, + "brace-expansion": { + "version": "1.1.11", + "bundled": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "case": { + "version": "1.6.3", + "bundled": true + }, + "concat-map": { + "version": "0.0.1", + "bundled": true + }, + "fs-extra": { + "version": "9.1.0", + "bundled": true, + "requires": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + } + }, + "graceful-fs": { + "version": "4.2.10", + "bundled": true + }, + "ignore": { + "version": "5.2.0", + "bundled": true + }, + "jsonfile": { + "version": "6.1.0", + "bundled": true, + "requires": { + "graceful-fs": "^4.1.6", + "universalify": "^2.0.0" + } + }, + "jsonschema": { + "version": "1.4.1", + "bundled": true + }, + "lru-cache": { + "version": "6.0.0", + "bundled": true, + "requires": { + "yallist": "^4.0.0" + } + }, + "minimatch": { + "version": "3.1.2", + "bundled": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "punycode": { + "version": "2.1.1", + "bundled": true + }, + "semver": { + "version": "7.3.7", + "bundled": true, + "requires": { + "lru-cache": "^6.0.0" + } + }, + "universalify": { + "version": "2.0.0", + "bundled": true + }, + "yallist": { + "version": "4.0.0", + "bundled": true + }, + "yaml": { + "version": "1.10.2", + "bundled": true + } + } + }, + "constructs": { + "version": "10.1.60", + "resolved": "https://registry.npmjs.org/constructs/-/constructs-10.1.60.tgz", + "integrity": "sha512-MI4MfAQu8M/Nk8/60/phguJXriYjfzpNr/Iww4EAfAogkW/5Td3Xb6B437kBG7ISFSiDzJ7v49uFhIsoAQB9nQ==" + } + } +} diff --git a/javascript/package.json b/javascript/package.json new file mode 100644 index 0000000..c087468 --- /dev/null +++ b/javascript/package.json @@ -0,0 +1,18 @@ +{ + "name": "app", + "version": "0.1.0", + "bin": { + "app": "app.js" + }, + "scripts": { + "build": "echo \"The build step is not required when using JavaScript!\" && exit 0", + "cdk": "cdk" + }, + "devDependencies": { + "aws-cdk": "2.33.0" + }, + "dependencies": { + "aws-cdk-lib": "2.33.0", + "constructs": "^10.0.0" + } +} diff --git a/javascript/recognition/infrastructure.js b/javascript/recognition/infrastructure.js new file mode 100644 index 0000000..4feea43 --- /dev/null +++ b/javascript/recognition/infrastructure.js @@ -0,0 +1,110 @@ +// declare SQS that reacts to image upload SNS +// declare SNS to where it sends the items + +const { Stack } = require('aws-cdk-lib'); +const iam = require('aws-cdk-lib/aws-iam'); +const ddb = require('aws-cdk-lib/aws-dynamodb'); +const lambda = require('aws-cdk-lib/aws-lambda'); +const apigateway = require('aws-cdk-lib/aws-apigateway'); + +class RekognitionStack extends Stack { + + constructor(scope, id, props) { + super(scope, id, props); + + // create new IAM group and user + const group = new iam.Group(this, "RekGroup") + const user = new iam.User(this, "RekUser") + + // add IAM user to the new group + user.addToGroup(group) + + // create DynamoDB table to hold Rekognition results + const table = new ddb.Table( + this, + "Classifications", { + partitionKey: { name: "image", type: ddb.AttributeType.STRING } + } + ) + + // create Lambda function + const lambdaFunction = new lambda.Function( + this, + "image_recognition", { + runtime: lambda.Runtime.NODEJS_16_X, + handler: "image-recognition.handler", + code: lambda.Code.fromAsset("recognition/runtime"), + environment: { + "TABLE_NAME": table.tableName, + "SQS_QUEUE_URL": props.sqsUrl, + "TOPIC_ARN": props.snsArn + } + } + ) + + lambdaFunction.addEventSourceMapping("ImgRekognitionLambda", { eventSourceArn: props.sqsArn }) + + // add Rekognition permissions for Lambda function + const rekognitionStatement = new iam.PolicyStatement() + rekognitionStatement.addActions("rekognition:DetectLabels") + rekognitionStatement.addResources("*") + lambdaFunction.addToRolePolicy(rekognitionStatement) + + // add SNS permissions for Lambda function + const snsPermission = new iam.PolicyStatement() + snsPermission.addActions("sns:publish") + snsPermission.addResources("*") + lambdaFunction.addToRolePolicy(snsPermission) + + // grant permission for lambda to receive/delete message from SQS + const sqsPermission = new iam.PolicyStatement() + sqsPermission.addActions("sqs:ChangeMessageVisibility") + sqsPermission.addActions("sqs:DeleteMessage") + sqsPermission.addActions("sqs:GetQueueAttributes") + sqsPermission.addActions("sqs:GetQueueUrl") + sqsPermission.addActions("sqs:ReceiveMessage") + sqsPermission.addResources("*") + lambdaFunction.addToRolePolicy(sqsPermission) + + // grant permissions for lambda to read/write to DynamoDB table + table.grantReadWriteData(lambdaFunction) + + // # grant permissions for lambda to read from bucket + const s3Permission = new iam.PolicyStatement() + s3Permission.addActions("s3:get*") + s3Permission.addResources("*") + lambdaFunction.addToRolePolicy(s3Permission) + + // add additional API Gateway and lambda to list ddb + const listImgLambda = new lambda.Function( + this, + "ListImagesLambda", { + functionName: "ListImagesLambda", + runtime: lambda.Runtime.NODEJS_16_X, + code: lambda.Code.fromAsset("recognition/runtime"), + handler: "list-images.handler", + environment: { "TABLE_NAME": table.tableName } + } + ) + + const api = new apigateway.RestApi( + this, + "REST_API", { + restApiName: "List Images Service", + description: "CW workshop - list images recognized from workshop." + } + ) + + const listImages = new apigateway.LambdaIntegration( + listImgLambda, { + requestTemplates: { "application/json": '{ "statusCode": "200" }' } + } + ) + + api.root.addMethod("GET", listImages) + + table.grantReadData(listImgLambda) + } +} + +module.exports = { RekognitionStack } diff --git a/javascript/recognition/runtime/image-recognition.js b/javascript/recognition/runtime/image-recognition.js new file mode 100644 index 0000000..26e3126 --- /dev/null +++ b/javascript/recognition/runtime/image-recognition.js @@ -0,0 +1,101 @@ +const AWS = require('aws-sdk') +const sqs = new AWS.SQS() +const rekognition = new AWS.Rekognition() +const dynamodb = new AWS.DynamoDB() +const sns = new AWS.SNS() + +const queueUrl = process.env.SQS_QUEUE_URL +const tableName = process.env.TABLE_NAME +const topicArn = process.env.TOPIC_ARN + +// 1.) Detect labels from image with Rekognition as "labels" +const detectImgLabels = async (bucketName, key, maxLabels = 10, minConfidence = 70) => { + const params = { + Image: { + S3Object: { + Bucket: bucketName, + Name: key + } + }, + MaxLabels: maxLabels, + MinConfidence: minConfidence + } + + const response = await rekognition.detectLabels(params).promise() + return response +} + +// 2.) Save labels to DynamoDB +const writeToDynamoDb = async (tableName, item) => { + const params = { + TableName: tableName, + Item: item + } + + await dynamodb.putItem(params).promise() +} + +// 3.) Publish item to SNS +const triggerSNS = async (message) => { + const params = { + Message: message, + Subject: "CodeWhisperer Workshop Success!", + TopicArn: topicArn + } + + const response = await sns.publish(params).promise() + console.log(response) +} + +// 4.) Delete message from SQS +const deleteFromSqs = async (receiptHandle) => { + const params = { + QueueUrl: queueUrl, + ReceiptHandle: receiptHandle + } + + await sqs.deleteMessage(params).promise() +} + +exports.handler = async function (event, context) { + try { + var key, bucketName; + + // process message from SQS + for (var eventRecord of event.Records) { + const receiptHandle = eventRecord.receiptHandle + for (var record of JSON.parse(eventRecord.body).Records) { + bucketName = record.s3.bucket.name + key = record.s3.object.key + + // call method 1.) to generate image label and store as var "labels" + const labels = await detectImgLabels(bucketName, key) + console.log(key, labels.Labels) + + // code snippet to create dynamodb item from labels + const dbResult = [] + for (var label of labels.Labels) { + dbResult.push(label.Name) + } + + const dbItem = { + "image": { "S": key }, + "labels": { "S": JSON.stringify(dbResult) } + } + + // call method 2.) to store "dbItem" result on DynamoDB + await writeToDynamoDb(tableName, dbItem) + + // call method 3.) to send message to SNS + await triggerSNS(JSON.stringify(dbResult)) + + // call method 4.) to delete img from SQS + await deleteFromSqs(receiptHandle) + } + } + } catch (error) { + console.log(error) + console.log(`Error processing object ${key} from bucket ${bucketName}.`) + throw error + } +} diff --git a/javascript/recognition/runtime/list-images.js b/javascript/recognition/runtime/list-images.js new file mode 100644 index 0000000..0cfdc79 --- /dev/null +++ b/javascript/recognition/runtime/list-images.js @@ -0,0 +1,20 @@ +const AWS = require('aws-sdk') +const ddb = new AWS.DynamoDB() +const tableName = process.env.TABLE_NAME + +// 1.) Function to list all items from a DynamoDB table +const getAllItemsFromTable = async (tableName) => { + return ddb.scan({ + TableName: tableName + }).promise() +} + +exports.handler = async function (event, context) { + // call method 1.) to scan items from DynamoDB + const response = await getAllItemsFromTable(tableName) + + return { + body: JSON.stringify(response.Items), + statusCode: 200 + } +}