├── .gitignore ├── Gruntfile.js ├── LICENSE ├── README.md ├── cfn ├── custom-resources │ └── log-group.py └── replicator.cfn.json ├── lambda ├── api │ ├── metrics │ │ └── get.js │ └── tables │ │ ├── get.js │ │ ├── prefixes │ │ ├── delete.js │ │ └── post.js │ │ └── replications │ │ ├── delete.js │ │ └── post.js ├── controller │ ├── create-replica │ │ └── index.js │ ├── index.js │ ├── start-replication │ │ └── index.js │ ├── stop-replication │ │ └── index.js │ ├── validate-replica │ │ └── index.js │ └── validate-source │ │ └── index.js ├── logger.js ├── replicator │ ├── index.js │ └── metrics.js └── watcher │ ├── alarm │ ├── create.js │ └── delete.js │ └── prefix │ ├── create.js │ ├── delete.js │ └── init.js ├── package-lock.json └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | util 4 | .min.js 5 | .idea 6 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | module.exports = function(grunt) { 2 | grunt.initConfig({ 3 | terser: { 4 | dist: { 5 | files: [ 6 | { 7 | src: 'lambda/**/*.js', 8 | ext: '.min.js', 9 | expand: true 10 | } 11 | ] 12 | } 13 | }, 14 | cfninit: { 15 | src: 'cfn/replicator.cfn.json', 16 | dest: 'dist/replicator.cfn.json' 17 | }, 18 | clean: [ 'lambda/**/*.min.js' ] 19 | }); 20 | 21 | //Load plugins for creating and removing minified javascript files 22 | grunt.loadNpmTasks('grunt-contrib-clean'); 23 | grunt.loadNpmTasks('grunt-terser'); 24 | 25 | //Register task for compiling cfn template 26 | grunt.registerTask('cfn-include', 'Build cloudformation template using cfn-include', function(){ 27 | var compileTemplate = require('cfn-include'); 28 | var path = require('path'); 29 | var config = grunt.config.get('cfninit'); 30 | 31 | var getAbsolutePath = function(filePath){ 32 | if(!path.isAbsolute(filePath)) 33 | filePath = path.join(process.cwd(), filePath); 34 | return filePath; 35 | }; 36 | 37 | //Build source and destination URLS for cfn templates 38 | var srcUrl = "file://" + getAbsolutePath(config.src); 39 | 40 | //Compile source template 41 | var done = this.async(); 42 | compileTemplate({ url: srcUrl }).then(function(template){ 43 | //Write compiled template to dest 44 | grunt.file.write(getAbsolutePath(config.dest), JSON.stringify(template, null, 2)); 45 | done(); 46 | }); 47 | }); 48 | 49 | grunt.registerTask('default', ['terser', 'cfn-include', 'clean']); 50 | }; 51 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | The MIT License (MIT) 3 | 4 | Copyright (c) 2016 Signiant 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DynamoDB Replication 2 | The dynamodb replication solution makes use of dynamodb table streams and lambda functions to replicate data cross regions in near real time. 3 | 4 | ## Documentation 5 | This README serves as a 'quick start' guide for getting replication up and running. Full documentation can be found in the [wiki](https://github.com/Signiant/dynamodb-replication/wiki). 6 | 7 | ## Deployment 8 | DymanoDB replication infrastructure is managed entirely through cloudformation. 9 | To deploy, simply use the cloudformation template from the latest [release](https://github.com/Signiant/dynamodb-replication/releases) to create a new cloudformation stack. 10 | 11 | ## Building The Template 12 | If you're making any changes of your own to the code, you will need to generate a new cloudformation template before you can deploy. To do this, execute the following steps: 13 | 14 | 1. Install dependencies: 15 | ``` 16 | $ npm install 17 | ``` 18 | 19 | 2. Build the template: 20 | ``` 21 | $ npm run build 22 | ``` 23 | 24 | The generated template will be output to 25 | ``` 26 | dist/replication.cfn.json 27 | ``` 28 | 29 | 3. The json file can be deployed to aws through 30 | ``` 31 | aws cloudformation package \ 32 | --template-file dist/replicator.cfn.json \ 33 | --s3-bucket dynamodb-replication \ 34 | --output-template-file packaged-template.yaml \ 35 | --profile 36 | ``` 37 | -------------------------------------------------------------------------------- /cfn/custom-resources/log-group.py: -------------------------------------------------------------------------------- 1 | import cfnresponse 2 | import boto3 3 | import botocore 4 | import json 5 | 6 | def handler(event, context): 7 | client = boto3.client('logs') 8 | responseStatus = cfnresponse.SUCCESS 9 | responseData = {} 10 | 11 | if event['RequestType'] == 'Create' or event['RequestType'] == 'Update': 12 | groupName = '/aws/lambda/%s' % event['ResourceProperties']['FunctionName'] 13 | try: 14 | response = client.create_log_group(logGroupName=groupName) 15 | responseData['LogGroupName'] = groupName 16 | 17 | except botocore.exceptions.ClientError as e: 18 | if e.response['Error']['Code'] == 'ResourceAlreadyExistsException': 19 | responseData['LogGroupName'] = groupName 20 | else: 21 | responseData['Error'] = e.response['Error']['Message'] 22 | responseStatus = cfnresponse.FAILED 23 | 24 | cfnresponse.send(event, context, responseStatus, responseData) 25 | -------------------------------------------------------------------------------- /cfn/replicator.cfn.json: -------------------------------------------------------------------------------- 1 | { 2 | "AWSTemplateFormatVersion" : "2010-09-09", 3 | "Metadata": { 4 | "AWS::CloudFormation::Interface": { 5 | "ParameterGroups": [ 6 | { 7 | "Label": { "default": "Replication Configuration" }, 8 | "Parameters": [ "ReplicaRegion" ] 9 | }, 10 | { 11 | "Label": { "default": "Replication Resources" }, 12 | "Parameters": [ "ControllerTableName", "PrefixTableName", "ApiName" ] 13 | }, 14 | { 15 | "Label": { "default": "Delay Monitoring" }, 16 | "Parameters": [ "DelayThreshold", "DelayNotifications", "DelayEndpointProtocol", "DelayEndpoint" ] 17 | } 18 | ] 19 | } 20 | }, 21 | "Parameters": { 22 | "ReplicaRegion": { 23 | "Type": "String", 24 | "Default": "us-west-2", 25 | "Description": "The destination region for table replication" 26 | }, 27 | "ControllerTableName": { 28 | "Type": "String", 29 | "Default": "ReplicationController", 30 | "Description": "Name of the DynamoDB table that holds replication metadata" 31 | }, 32 | "PrefixTableName": { 33 | "Type": "String", 34 | "Default": "ReplicationPrefixes", 35 | "Description": "Name of the DynamoDB table that holds the replication prefixes" 36 | }, 37 | "ApiName": { 38 | "Type": "String", 39 | "Default": "ReplicationApi", 40 | "Description": "Name of the rest API used to manage replication" 41 | }, 42 | "DelayThreshold": { 43 | "Type": "Number", 44 | "Description": "How far behind does the replication need to be (in minutes) to trigger the alarm to send a notification?", 45 | "Default": 5, 46 | "MinValue": 2 47 | }, 48 | "DelayNotifications": { 49 | "Type": "String", 50 | "Default": "true", 51 | "AllowedValues": [ "true", "false" ], 52 | "Description": "Set to true if you want to recieve notifications if a replica table falls too far behind the master. If not, set to false and ignore the following two parameters in this group." 53 | }, 54 | 55 | "DelayEndpointProtocol":{ 56 | "Type": "String", 57 | "Default": "email", 58 | "AllowedValues": [ "http", "https", "email", "email-json", "sms", "sqs", "application", "lambda" ], 59 | "Description": "The protocol of the notification endpoint." 60 | }, 61 | "DelayEndpoint": { 62 | "Type": "String", 63 | "Description": "The endpoint to send notifications to." 64 | } 65 | }, 66 | "Conditions": { 67 | "NotifyOnDelay": { 68 | "Fn::Equals": [ 69 | { "Ref": "DelayNotifications" }, 70 | "true" 71 | ] 72 | } 73 | }, 74 | "Resources": { 75 | "ReplicatorRole": { 76 | "Type": "AWS::IAM::Role", 77 | "Properties": { 78 | "Path": "/replication/replicator/", 79 | "AssumeRolePolicyDocument" : { 80 | "Version": "2012-10-17", 81 | "Statement": [{ 82 | "Effect": "Allow", 83 | "Principal": {"Service": ["lambda.amazonaws.com"]}, 84 | "Action": ["sts:AssumeRole"] 85 | }] 86 | }, 87 | "Policies": [ 88 | { 89 | "PolicyName": "ReplicationReplicatorPolicy", 90 | "PolicyDocument": { 91 | "Version": "2012-10-17", 92 | "Statement": [ 93 | { 94 | "Sid": "Logging", 95 | "Action": [ 96 | "logs:CreateLogGroup", 97 | "logs:CreateLogStream", 98 | "logs:PutLogEvents" 99 | ], 100 | "Effect": "Allow", 101 | "Resource": "arn:aws:logs:*:*:*:*" 102 | }, 103 | { 104 | "Sid": "InvokeSelf", 105 | "Action": [ 106 | "lambda:InvokeFunction" 107 | ], 108 | "Effect": "Allow", 109 | "Resource": "*" 110 | }, 111 | { 112 | "Sid": "SourceStream", 113 | "Action": [ 114 | "dynamodb:GetRecords", 115 | "dynamodb:GetShardIterator", 116 | "dynamodb:DescribeStream", 117 | "dynamodb:ListStreams" 118 | ], 119 | "Effect": "Allow", 120 | "Resource": "arn:aws:dynamodb:*:*:table/*/stream/*" 121 | }, 122 | { 123 | "Sid": "ReplicaTable", 124 | "Action": [ 125 | "dynamodb:BatchWriteItem" 126 | ], 127 | "Effect": "Allow", 128 | "Resource": { "Fn::Join": [ ":", [ "arn", "aws", "dynamodb", { "Ref": "ReplicaRegion" }, "*", "table/*"]]} 129 | } 130 | ] 131 | } 132 | } 133 | ] 134 | } 135 | }, 136 | "ReplicatorFunction": { 137 | "Type": "AWS::Lambda::Function", 138 | "Properties": { 139 | "FunctionName": "Replication-Replicator", 140 | "Description": "Replicate dynamodb table data across regions", 141 | "Role": { "Fn::GetAtt": [ "ReplicatorRole", "Arn" ] }, 142 | "Runtime": "nodejs10.x", 143 | "Handler": "index.handler", 144 | "MemorySize": 128, 145 | "Timeout": 300, 146 | "Code": { 147 | "ZipFile": { 148 | "Fn::Include": { 149 | "type": "literal", 150 | "location": "../lambda/replicator/index.min.js", 151 | "context": { 152 | "replicaRegion": { "Ref": "ReplicaRegion" } 153 | } 154 | } 155 | } 156 | } 157 | } 158 | }, 159 | "MetricRole": { 160 | "Type": "AWS::IAM::Role", 161 | "Properties": { 162 | "Path": "/replication/metrics/", 163 | "AssumeRolePolicyDocument" : { 164 | "Version": "2012-10-17", 165 | "Statement": [{ 166 | "Effect": "Allow", 167 | "Principal": {"Service": ["lambda.amazonaws.com"]}, 168 | "Action": ["sts:AssumeRole"] 169 | }] 170 | }, 171 | "Policies": [ 172 | { 173 | "PolicyName": "ReplicationMetricPolicy", 174 | "PolicyDocument": { 175 | "Version": "2012-10-17", 176 | "Statement": [ 177 | { 178 | "Effect": "Allow", 179 | "Action": [ 180 | "cloudwatch:PutMetricData" 181 | ], 182 | "Resource": "*" 183 | }, 184 | { 185 | "Effect": "Allow", 186 | "Action": [ 187 | "logs:CreateLogGroup", 188 | "logs:CreateLogStream", 189 | "logs:PutLogEvents" 190 | ], 191 | "Resource": "arn:aws:logs:*:*:*" 192 | } 193 | ] 194 | } 195 | } 196 | ] 197 | } 198 | }, 199 | "MetricFunction": { 200 | "Type": "AWS::Lambda::Function", 201 | "Properties": { 202 | "FunctionName": "Replication-Metrics", 203 | "Description": "Post custom replication metrics to CloudWatch, parsed from a log subscription", 204 | "Role": { "Fn::GetAtt": [ "MetricRole", "Arn" ] }, 205 | "Runtime": "nodejs10.x", 206 | "Handler": "index.handler", 207 | "MemorySize": 128, 208 | "Timeout": 300, 209 | "Code": { 210 | "ZipFile": { 211 | "Fn::Include": { 212 | "type": "literal", 213 | "location": "../lambda/replicator/metrics.min.js" 214 | } 215 | } 216 | } 217 | } 218 | }, 219 | "MetricSubscription": { 220 | "Type": "AWS::Logs::SubscriptionFilter", 221 | "DependsOn": "MetricPermission", 222 | "Properties": { 223 | "LogGroupName": { "Fn::GetAtt": [ "ReplicatorLogGroup", "LogGroupName" ] }, 224 | "FilterPattern": "[timestamp, request_id, metric=METRIC, table, level, name, value]", 225 | "DestinationArn": { "Fn::GetAtt": [ "MetricFunction", "Arn" ] } 226 | } 227 | }, 228 | "MetricPermission": { 229 | "Type": "AWS::Lambda::Permission", 230 | "Properties": { 231 | "FunctionName": { "Fn::GetAtt": [ "MetricFunction", "Arn" ] }, 232 | "Action": "lambda:InvokeFunction", 233 | "Principal": { "Fn::Join": [ ".", [ "logs", { "Ref": "AWS::Region" }, "amazonaws", "com" ] ] }, 234 | "SourceArn": { "Fn::Join": [ ":", [ "arn", "aws", "logs", { "Ref": "AWS::Region" }, { "Ref": "AWS::AccountId" }, "log-group", { "Fn::GetAtt": [ "ReplicatorLogGroup", "LogGroupName" ] }, "*" ] ] } 235 | } 236 | }, 237 | "ReplicatorLogGroup": { 238 | "Type": "Custom::LogGroup", 239 | "DeletionPolicy": "Retain", 240 | "Properties": { 241 | "ServiceToken": { "Fn::GetAtt" : [ "CreateLogGroupFunction", "Arn" ] }, 242 | "FunctionName": { "Ref": "ReplicatorFunction" } 243 | } 244 | }, 245 | "CreateLogGroupRole": { 246 | "Type": "AWS::IAM::Role", 247 | "Properties": { 248 | "Path": "/replication/cfn/loggroup/", 249 | "AssumeRolePolicyDocument" : { 250 | "Version": "2012-10-17", 251 | "Statement": [{ 252 | "Effect": "Allow", 253 | "Principal": {"Service": ["lambda.amazonaws.com"]}, 254 | "Action": ["sts:AssumeRole"] 255 | }] 256 | }, 257 | "Policies": [ 258 | { 259 | "PolicyName": "CreateLogGroupPolicy", 260 | "PolicyDocument": { 261 | "Version": "2012-10-17", 262 | "Statement": [ 263 | { 264 | "Effect": "Allow", 265 | "Action": [ 266 | "logs:CreateLogGroup", 267 | "logs:CreateLogStream", 268 | "logs:PutLogEvents" 269 | ], 270 | "Resource": "arn:aws:logs:*:*:*" 271 | } 272 | ] 273 | } 274 | } 275 | ] 276 | } 277 | }, 278 | "CreateLogGroupFunction": { 279 | "Type": "AWS::Lambda::Function", 280 | "Properties": { 281 | "FunctionName": "Replicator-Cfn-LogGroup", 282 | "Description": "Endpoint for cloudformation custom resource. Creates log group for the main replicator function", 283 | "Handler": "index.handler", 284 | "Runtime": "python2.7", 285 | "Timeout": "60", 286 | "Role": { "Fn::GetAtt" : [ "CreateLogGroupRole", "Arn" ] }, 287 | "Code": { 288 | "ZipFile": { 289 | "Fn::Include": { 290 | "type": "literal", 291 | "location": "custom-resources/log-group.py" 292 | } 293 | } 294 | } 295 | } 296 | }, 297 | "ControllerTable": { 298 | "Type": "AWS::DynamoDB::Table", 299 | "Properties": { 300 | "TableName": { "Ref": "ControllerTableName" }, 301 | "AttributeDefinitions": [ 302 | { 303 | "AttributeName": "tableName", 304 | "AttributeType": "S" 305 | } 306 | ], 307 | "KeySchema": [ 308 | { 309 | "AttributeName": "tableName", 310 | "KeyType": "HASH" 311 | } 312 | ], 313 | "ProvisionedThroughput": { 314 | "ReadCapacityUnits": 5, 315 | "WriteCapacityUnits": 5 316 | }, 317 | "StreamSpecification": { 318 | "StreamViewType": "NEW_AND_OLD_IMAGES" 319 | } 320 | } 321 | }, 322 | "PrefixTable": { 323 | "Type": "AWS::DynamoDB::Table", 324 | "Properties": { 325 | "TableName": { "Ref": "PrefixTableName" }, 326 | "AttributeDefinitions": [ 327 | { 328 | "AttributeName": "prefix", 329 | "AttributeType": "S" 330 | } 331 | ], 332 | "KeySchema": [ 333 | { 334 | "AttributeName": "prefix", 335 | "KeyType": "HASH" 336 | } 337 | ], 338 | "ProvisionedThroughput": { 339 | "ReadCapacityUnits": 5, 340 | "WriteCapacityUnits": 5 341 | }, 342 | "StreamSpecification": { 343 | "StreamViewType": "NEW_AND_OLD_IMAGES" 344 | } 345 | } 346 | }, 347 | "ControllerRole": { 348 | "Type": "AWS::IAM::Role", 349 | "Properties": { 350 | "Path": "/replication/controller/", 351 | "AssumeRolePolicyDocument" : { 352 | "Version": "2012-10-17", 353 | "Statement": [{ 354 | "Effect": "Allow", 355 | "Principal": {"Service": ["lambda.amazonaws.com"]}, 356 | "Action": ["sts:AssumeRole"] 357 | }] 358 | }, 359 | "Policies": [ 360 | { 361 | "PolicyName": "ReplicationControllerPolicy", 362 | "PolicyDocument": { 363 | "Version": "2012-10-17", 364 | "Statement": [ 365 | { 366 | "Sid": "LambdaLogging", 367 | "Action": [ 368 | "logs:CreateLogGroup", 369 | "logs:CreateLogStream", 370 | "logs:PutLogEvents" 371 | ], 372 | "Effect": "Allow", 373 | "Resource": "arn:aws:logs:*:*:*:*" 374 | }, 375 | { 376 | "Sid": "InvokeFunctions", 377 | "Action": [ 378 | "lambda:InvokeFunction" 379 | ], 380 | "Effect": "Allow", 381 | "Resource": "*" 382 | }, 383 | { 384 | "Sid": "SourceStream", 385 | "Action": [ 386 | "dynamodb:GetRecords", 387 | "dynamodb:GetShardIterator", 388 | "dynamodb:DescribeStream", 389 | "dynamodb:ListStreams" 390 | ], 391 | "Effect": "Allow", 392 | "Resource": { "Fn::Join": [ "", [ "arn:aws:dynamodb:", {"Ref": "AWS::Region"}, ":", { "Ref": "AWS::AccountId" }, ":table/", { "Ref": "ControllerTableName" }, "/stream/*" ] ] } 393 | }, 394 | { 395 | "Sid": "ReplicaTable", 396 | "Action": [ 397 | "dynamodb:UpdateItem", 398 | "dynamodb:DeleteItem" 399 | ], 400 | "Effect": "Allow", 401 | "Resource": { "Fn::Join": [ "", [ "arn:aws:dynamodb:", {"Ref": "AWS::Region"}, ":", { "Ref": "AWS::AccountId" }, ":table/", { "Ref": "ControllerTableName" } ] ] } 402 | } 403 | ] 404 | } 405 | } 406 | ] 407 | } 408 | }, 409 | "ControllerFunction": { 410 | "Type": "AWS::Lambda::Function", 411 | "Properties": { 412 | "FunctionName": "Replication-Controller", 413 | "Description": "Invokes lambda functions in response to changes in state and/or step on the replication controller table", 414 | "Role": { "Fn::GetAtt": [ "ControllerRole", "Arn" ] }, 415 | "Runtime": "nodejs10.x", 416 | "Handler": "index.handler", 417 | "MemorySize": 128, 418 | "Timeout": 300, 419 | "Code": { 420 | "ZipFile": { 421 | "Fn::Include": { 422 | "type": "literal", 423 | "location": "../lambda/controller/index.min.js", 424 | "context": { 425 | "replicatorFunction": { "Fn::GetAtt": [ "ReplicatorFunction", "Arn" ] }, 426 | "validateSourceFunction": { "Fn::GetAtt": [ "ValidateSourceFunction", "Arn" ] }, 427 | "validateReplicaFunction": { "Fn::GetAtt": [ "ValidateReplicaFunction", "Arn" ] }, 428 | "createReplicaFunction": { "Fn::GetAtt": [ "CreateReplicaFunction", "Arn" ] }, 429 | "startReplicationFunction": { "Fn::GetAtt": [ "StartReplicationFunction", "Arn" ] }, 430 | "stopReplicationFunction": { "Fn::GetAtt": [ "StopReplicationFunction", "Arn" ] } 431 | } 432 | } 433 | } 434 | } 435 | } 436 | }, 437 | "ControllerEvent": { 438 | "Type": "AWS::Lambda::EventSourceMapping", 439 | "Properties": { 440 | "FunctionName": { "Fn::GetAtt": [ "ControllerFunction", "Arn" ] }, 441 | "EventSourceArn": { "Fn::GetAtt": [ "ControllerTable", "StreamArn" ] }, 442 | "BatchSize": 1, 443 | "StartingPosition": "TRIM_HORIZON" 444 | } 445 | }, 446 | "ValidateSourceRole": { 447 | "Type": "AWS::IAM::Role", 448 | "Properties": { 449 | "Path": "/replication/controller/validate-source/", 450 | "AssumeRolePolicyDocument" : { 451 | "Version": "2012-10-17", 452 | "Statement": [{ 453 | "Effect": "Allow", 454 | "Principal": {"Service": ["lambda.amazonaws.com"]}, 455 | "Action": ["sts:AssumeRole"] 456 | }] 457 | }, 458 | "Policies": [ 459 | { 460 | "PolicyName": "ReplicationValidateSourcePolicy", 461 | "PolicyDocument": { 462 | "Version": "2012-10-17", 463 | "Statement": [ 464 | { 465 | "Sid": "LambdaLogging", 466 | "Action": [ 467 | "logs:CreateLogGroup", 468 | "logs:CreateLogStream", 469 | "logs:PutLogEvents" 470 | ], 471 | "Effect": "Allow", 472 | "Resource": "arn:aws:logs:*:*:*:*" 473 | }, 474 | { 475 | "Sid": "ValidateSourceTable", 476 | "Action": [ 477 | "dynamodb:DescribeTable", 478 | "dynamodb:ListTagsOfResource" 479 | ], 480 | "Effect": "Allow", 481 | "Resource": { "Fn::Join": [ ":", [ "arn", "aws", "dynamodb", {"Ref": "AWS::Region"}, { "Ref": "AWS::AccountId" }, "table/*" ] ] } 482 | } 483 | ] 484 | } 485 | } 486 | ] 487 | } 488 | }, 489 | "ValidateSourceFunction": { 490 | "Type": "AWS::Lambda::Function", 491 | "Properties": { 492 | "FunctionName": "Replication-ValidateSource", 493 | "Description": "Validates that the source table exists and is configured correctly for replication", 494 | "Role": { "Fn::GetAtt": [ "ValidateSourceRole", "Arn" ] }, 495 | "Runtime": "nodejs10.x", 496 | "Handler": "index.handler", 497 | "MemorySize": 128, 498 | "Timeout": 270, 499 | "Code": { 500 | "ZipFile": { 501 | "Fn::Include": { 502 | "type": "literal", 503 | "location": "../lambda/controller/validate-source/index.min.js" 504 | } 505 | } 506 | } 507 | } 508 | }, 509 | "ValidateReplicaRole": { 510 | "Type": "AWS::IAM::Role", 511 | "Properties": { 512 | "Path": "/replication/controller/validate-replica/", 513 | "AssumeRolePolicyDocument" : { 514 | "Version": "2012-10-17", 515 | "Statement": [{ 516 | "Effect": "Allow", 517 | "Principal": {"Service": ["lambda.amazonaws.com"]}, 518 | "Action": ["sts:AssumeRole"] 519 | }] 520 | }, 521 | "Policies": [ 522 | { 523 | "PolicyName": "ReplicationValidateReplicaPolicy", 524 | "PolicyDocument": { 525 | "Version": "2012-10-17", 526 | "Statement": [ 527 | { 528 | "Sid": "LambdaLogging", 529 | "Action": [ 530 | "logs:CreateLogGroup", 531 | "logs:CreateLogStream", 532 | "logs:PutLogEvents" 533 | ], 534 | "Effect": "Allow", 535 | "Resource": "arn:aws:logs:*:*:*:*" 536 | }, 537 | { 538 | "Sid": "ValidateReplicaTable", 539 | "Action": [ 540 | "dynamodb:DescribeTable" 541 | ], 542 | "Effect": "Allow", 543 | "Resource": { "Fn::Join": [ ":", [ "arn", "aws", "dynamodb", {"Ref": "ReplicaRegion"}, { "Ref": "AWS::AccountId" }, "table/*" ] ] } 544 | } 545 | ] 546 | } 547 | } 548 | ] 549 | } 550 | }, 551 | "ValidateReplicaFunction": { 552 | "Type": "AWS::Lambda::Function", 553 | "Properties": { 554 | "FunctionName": "Replication-ValidateReplica", 555 | "Description": "Check whether the replica table exists and has a key schema that matches the source", 556 | "Role": { "Fn::GetAtt": [ "ValidateReplicaRole", "Arn" ] }, 557 | "Runtime": "nodejs10.x", 558 | "Handler": "index.handler", 559 | "MemorySize": 128, 560 | "Timeout": 270, 561 | "Code": { 562 | "ZipFile": { 563 | "Fn::Include": { 564 | "type": "literal", 565 | "location": "../lambda/controller/validate-replica/index.min.js", 566 | "context": { 567 | "replicaRegion": { "Ref": "ReplicaRegion" } 568 | } 569 | } 570 | } 571 | } 572 | } 573 | }, 574 | "CreateReplicaRole": { 575 | "Type": "AWS::IAM::Role", 576 | "Properties": { 577 | "Path": "/replication/controller/create-replica/", 578 | "AssumeRolePolicyDocument" : { 579 | "Version": "2012-10-17", 580 | "Statement": [{ 581 | "Effect": "Allow", 582 | "Principal": {"Service": ["lambda.amazonaws.com"]}, 583 | "Action": ["sts:AssumeRole"] 584 | }] 585 | }, 586 | "Policies": [ 587 | { 588 | "PolicyName": "ReplicationCreateReplicaPolicy", 589 | "PolicyDocument": { 590 | "Version": "2012-10-17", 591 | "Statement": [ 592 | { 593 | "Sid": "LambdaLogging", 594 | "Action": [ 595 | "logs:CreateLogGroup", 596 | "logs:CreateLogStream", 597 | "logs:PutLogEvents" 598 | ], 599 | "Effect": "Allow", 600 | "Resource": "arn:aws:logs:*:*:*:*" 601 | }, 602 | { 603 | "Sid": "DescribeSource", 604 | "Action": [ 605 | "dynamodb:DescribeTable" 606 | ], 607 | "Effect": "Allow", 608 | "Resource": { "Fn::Join": [ ":", [ "arn", "aws", "dynamodb", { "Ref": "AWS::Region" }, { "Ref": "AWS::AccountId" }, "table/*" ]]} 609 | }, 610 | { 611 | "Sid": "CreateReplica", 612 | "Action": [ 613 | "dynamodb:CreateTable" 614 | ], 615 | "Effect": "Allow", 616 | "Resource": { "Fn::Join": [ ":", [ "arn", "aws", "dynamodb", {"Ref": "ReplicaRegion" }, { "Ref": "AWS::AccountId" }, "table/*" ] ] } 617 | } 618 | ] 619 | } 620 | } 621 | ] 622 | } 623 | }, 624 | "CreateReplicaFunction" : { 625 | "Type": "AWS::Lambda::Function", 626 | "Properties": { 627 | "FunctionName": "Replication-CreateReplica", 628 | "Description": "Creates a replica table to match the source", 629 | "Role": { "Fn::GetAtt": [ "CreateReplicaRole", "Arn" ] }, 630 | "Runtime": "nodejs10.x", 631 | "Handler": "index.handler", 632 | "MemorySize": 128, 633 | "Timeout": 270, 634 | "Code": { 635 | "ZipFile": { 636 | "Fn::Include": { 637 | "type": "literal", 638 | "location": "../lambda/controller/create-replica/index.min.js", 639 | "context": { 640 | "replicaRegion": { "Ref": "ReplicaRegion" } 641 | } 642 | } 643 | } 644 | } 645 | } 646 | }, 647 | "StartReplicationRole": { 648 | "Type": "AWS::IAM::Role", 649 | "Properties": { 650 | "Path": "/replication/controller/start-replication/", 651 | "AssumeRolePolicyDocument" : { 652 | "Version": "2012-10-17", 653 | "Statement": [{ 654 | "Effect": "Allow", 655 | "Principal": {"Service": ["lambda.amazonaws.com"]}, 656 | "Action": ["sts:AssumeRole"] 657 | }] 658 | }, 659 | "Policies": [ 660 | { 661 | "PolicyName": "ReplicationStartReplicationPolicy", 662 | "PolicyDocument": { 663 | "Version": "2012-10-17", 664 | "Statement": [ 665 | { 666 | "Sid": "LambdaLogging", 667 | "Action": [ 668 | "logs:CreateLogGroup", 669 | "logs:CreateLogStream", 670 | "logs:PutLogEvents" 671 | ], 672 | "Effect": "Allow", 673 | "Resource": "arn:aws:logs:*:*:*:*" 674 | }, 675 | { 676 | "Sid": "CreateEventSource", 677 | "Action": [ 678 | "lambda:CreateEventSourceMapping" 679 | ], 680 | "Effect": "Allow", 681 | "Resource": "*" 682 | } 683 | ] 684 | } 685 | } 686 | ] 687 | } 688 | }, 689 | "StartReplicationFunction": { 690 | "Type": "AWS::Lambda::Function", 691 | "Properties": { 692 | "FunctionName": "Replication-StartReplication", 693 | "Description": "Starts the replication process by adding an event source mapping to the replicator function", 694 | "Role": { "Fn::GetAtt": [ "StartReplicationRole", "Arn" ] }, 695 | "Runtime": "nodejs10.x", 696 | "Handler": "index.handler", 697 | "MemorySize": 128, 698 | "Timeout": 270, 699 | "Code": { 700 | "ZipFile": { 701 | "Fn::Include": { 702 | "type": "literal", 703 | "location": "../lambda/controller/start-replication/index.min.js" 704 | } 705 | } 706 | } 707 | } 708 | }, 709 | "StopReplicationRole": { 710 | "Type": "AWS::IAM::Role", 711 | "Properties": { 712 | "Path": "/replication/controller/start-replication/", 713 | "AssumeRolePolicyDocument" : { 714 | "Version": "2012-10-17", 715 | "Statement": [{ 716 | "Effect": "Allow", 717 | "Principal": {"Service": ["lambda.amazonaws.com"]}, 718 | "Action": ["sts:AssumeRole"] 719 | }] 720 | }, 721 | "Policies": [ 722 | { 723 | "PolicyName": "ReplicationStartReplicationPolicy", 724 | "PolicyDocument": { 725 | "Version": "2012-10-17", 726 | "Statement": [ 727 | { 728 | "Sid": "LambdaLogging", 729 | "Action": [ 730 | "logs:CreateLogGroup", 731 | "logs:CreateLogStream", 732 | "logs:PutLogEvents" 733 | ], 734 | "Effect": "Allow", 735 | "Resource": "arn:aws:logs:*:*:*:*" 736 | }, 737 | { 738 | "Sid": "DeleteEventSource", 739 | "Action": [ 740 | "lambda:DeleteEventSourceMapping" 741 | ], 742 | "Effect": "Allow", 743 | "Resource": "*" 744 | } 745 | ] 746 | } 747 | } 748 | ] 749 | } 750 | }, 751 | "StopReplicationFunction": { 752 | "Type": "AWS::Lambda::Function", 753 | "Properties": { 754 | "FunctionName": "Replication-StopReplication", 755 | "Description": "Stops the replication process by removing the event source mapping from the replicator function", 756 | "Role": { "Fn::GetAtt": [ "StopReplicationRole", "Arn" ] }, 757 | "Runtime": "nodejs10.x", 758 | "Handler": "index.handler", 759 | "MemorySize": 128, 760 | "Timeout": 270, 761 | "Code": { 762 | "ZipFile": { 763 | "Fn::Include": { 764 | "type": "literal", 765 | "location": "../lambda/controller/stop-replication/index.min.js" 766 | } 767 | } 768 | } 769 | } 770 | }, 771 | "WatcherPrefixCreateRole": { 772 | "Type": "AWS::IAM::Role", 773 | "Properties": { 774 | "Path": "/replication/watcher/prefix/create/", 775 | "AssumeRolePolicyDocument" : { 776 | "Version": "2012-10-17", 777 | "Statement": [{ 778 | "Effect": "Allow", 779 | "Principal": {"Service": ["lambda.amazonaws.com"]}, 780 | "Action": ["sts:AssumeRole"] 781 | }] 782 | }, 783 | "Policies": [ 784 | { 785 | "PolicyName": "ReplicationWatcherPrefixCreatePolicy", 786 | "PolicyDocument": { 787 | "Version": "2012-10-17", 788 | "Statement": [ 789 | { 790 | "Sid": "LambdaLogging", 791 | "Action": [ 792 | "logs:CreateLogGroup", 793 | "logs:CreateLogStream", 794 | "logs:PutLogEvents" 795 | ], 796 | "Effect": "Allow", 797 | "Resource": "arn:aws:logs:*:*:*:*" 798 | }, 799 | { 800 | "Sid": "GetPrefixes", 801 | "Action": [ 802 | "dynamodb:Scan" 803 | ], 804 | "Effect": "Allow", 805 | "Resource": { "Fn::Join": [ "", [ "arn:aws:dynamodb:", { "Ref": "AWS::Region" }, ":", { "Ref": "AWS::AccountId" }, ":table/", { "Ref": "PrefixTableName" } ] ] } 806 | }, 807 | { 808 | "Sid": "AddReplication", 809 | "Action": [ 810 | "dynamodb:PutItem" 811 | ], 812 | "Effect": "Allow", 813 | "Resource": { "Fn::Join": [ "", [ "arn:aws:dynamodb:", { "Ref": "AWS::Region" }, ":", { "Ref": "AWS::AccountId" }, ":table/", { "Ref": "ControllerTableName" } ] ] } 814 | } 815 | ] 816 | } 817 | } 818 | ] 819 | } 820 | }, 821 | "WatcherPrefixCreateFunction": { 822 | "Type": "AWS::Lambda::Function", 823 | "Properties": { 824 | "FunctionName": "Replication-Watcher-Prefix-Create", 825 | "Description": "Starts replication for new tables matching one of a list of prefixes (stored in dynamodb)", 826 | "Role": { "Fn::GetAtt": [ "WatcherPrefixCreateRole", "Arn" ] }, 827 | "Runtime": "nodejs10.x", 828 | "Handler": "index.handler", 829 | "MemorySize": 128, 830 | "Timeout": 260, 831 | "Code": { 832 | "ZipFile": { 833 | "Fn::Include": { 834 | "type": "literal", 835 | "location": "../lambda/watcher/prefix/create.min.js", 836 | "context": { 837 | "prefixTable": { "Ref": "PrefixTableName" }, 838 | "controllerTable": { "Ref": "ControllerTableName" } 839 | } 840 | } 841 | } 842 | } 843 | } 844 | }, 845 | "WatcherPrefixCreateRule": { 846 | "Type": "AWS::Events::Rule", 847 | "Properties": { 848 | "Description": "Invoke the replication watcher prefix create function when new DynamoDB tables are created", 849 | "Name": "Replication-Watcher-Prefix-Create", 850 | "State": "ENABLED", 851 | "EventPattern": { 852 | "source": [ 853 | "aws.dynamodb", 854 | "replication.watcher.prefix.init" 855 | ], 856 | "detail": { 857 | "eventSource": [ 858 | "dynamodb.amazonaws.com" 859 | ], 860 | "eventName": [ 861 | "CreateTable" 862 | ] 863 | } 864 | }, 865 | "Targets": [ 866 | { 867 | "Arn": { "Fn::GetAtt": [ "WatcherPrefixCreateFunction", "Arn" ] }, 868 | "Id": "Replication-Watcher-Prefix-Create" 869 | } 870 | ] 871 | } 872 | }, 873 | "WatcherPrefixCreatePermission": { 874 | "Type": "AWS::Lambda::Permission", 875 | "Properties": { 876 | "Action": "lambda:InvokeFunction", 877 | "FunctionName": { "Fn::GetAtt": [ "WatcherPrefixCreateFunction", "Arn" ] }, 878 | "Principal": "events.amazonaws.com", 879 | "SourceArn": { "Fn::GetAtt": [ "WatcherPrefixCreateRule", "Arn" ] } 880 | } 881 | }, 882 | "WatcherPrefixDeleteRole": { 883 | "Type": "AWS::IAM::Role", 884 | "Properties": { 885 | "Path": "/replication/watcher/prefix/delete/", 886 | "AssumeRolePolicyDocument" : { 887 | "Version": "2012-10-17", 888 | "Statement": [{ 889 | "Effect": "Allow", 890 | "Principal": {"Service": ["lambda.amazonaws.com"]}, 891 | "Action": ["sts:AssumeRole"] 892 | }] 893 | }, 894 | "Policies": [ 895 | { 896 | "PolicyName": "ReplicationWatcherPrefixDeletePolicy", 897 | "PolicyDocument": { 898 | "Version": "2012-10-17", 899 | "Statement": [ 900 | { 901 | "Sid": "LambdaLogging", 902 | "Action": [ 903 | "logs:CreateLogGroup", 904 | "logs:CreateLogStream", 905 | "logs:PutLogEvents" 906 | ], 907 | "Effect": "Allow", 908 | "Resource": "arn:aws:logs:*:*:*:*" 909 | }, 910 | { 911 | "Sid": "StopReplication", 912 | "Action": [ 913 | "dynamodb:UpdateItem" 914 | ], 915 | "Effect": "Allow", 916 | "Resource": { "Fn::Join": [ "", [ "arn:aws:dynamodb:", { "Ref": "AWS::Region" }, ":", { "Ref": "AWS::AccountId" }, ":table/", { "Ref": "ControllerTableName" } ] ] } 917 | } 918 | ] 919 | } 920 | } 921 | ] 922 | } 923 | }, 924 | "WatcherPrefixDeleteFunction": { 925 | "Type": "AWS::Lambda::Function", 926 | "Properties": { 927 | "FunctionName": "Replication-Watcher-Prefix-Delete", 928 | "Description": "Stops/removes replication when a source dynamodb table is deleted", 929 | "Role": { "Fn::GetAtt": [ "WatcherPrefixDeleteRole", "Arn" ] }, 930 | "Runtime": "nodejs10.x", 931 | "Handler": "index.handler", 932 | "MemorySize": 128, 933 | "Timeout": 300, 934 | "Code": { 935 | "ZipFile": { 936 | "Fn::Include": { 937 | "type": "literal", 938 | "location": "../lambda/watcher/prefix/delete.min.js", 939 | "context": { 940 | "controllerTable": { "Ref": "ControllerTableName" } 941 | } 942 | } 943 | } 944 | } 945 | } 946 | }, 947 | "WatcherPrefixDeleteRule": { 948 | "Type": "AWS::Events::Rule", 949 | "Properties": { 950 | "Description": "Invoke the replication watcher prefix delete function when any DynamoDB tables are deleted", 951 | "Name": "Replication-Watcher-Prefix-Delete", 952 | "State": "ENABLED", 953 | "EventPattern": { 954 | "detail-type": [ 955 | "AWS API Call via CloudTrail" 956 | ], 957 | "detail": { 958 | "eventSource": [ 959 | "dynamodb.amazonaws.com" 960 | ], 961 | "eventName": [ 962 | "DeleteTable" 963 | ] 964 | } 965 | }, 966 | "Targets": [ 967 | { 968 | "Arn": { "Fn::GetAtt": [ "WatcherPrefixDeleteFunction", "Arn" ] }, 969 | "Id": "Replication-Watcher-Prefix-Delete" 970 | } 971 | ] 972 | } 973 | }, 974 | "WatcherPrefixDeletePermission": { 975 | "Type": "AWS::Lambda::Permission", 976 | "Properties": { 977 | "Action": "lambda:InvokeFunction", 978 | "FunctionName": { "Fn::GetAtt": [ "WatcherPrefixDeleteFunction", "Arn" ] }, 979 | "Principal": "events.amazonaws.com", 980 | "SourceArn": { "Fn::GetAtt": [ "WatcherPrefixDeleteRule", "Arn" ] } 981 | } 982 | }, 983 | "WatcherPrefixInitRole": { 984 | "Type": "AWS::IAM::Role", 985 | "Properties": { 986 | "Path": "/replication/watcher/prefix/init/", 987 | "AssumeRolePolicyDocument" : { 988 | "Version": "2012-10-17", 989 | "Statement": [{ 990 | "Effect": "Allow", 991 | "Principal": {"Service": ["lambda.amazonaws.com"]}, 992 | "Action": ["sts:AssumeRole"] 993 | }] 994 | }, 995 | "Policies": [ 996 | { 997 | "PolicyName": "ReplicationWatcherPrefixInitPolicy", 998 | "PolicyDocument": { 999 | "Version": "2012-10-17", 1000 | "Statement": [ 1001 | { 1002 | "Sid": "LambdaLogging", 1003 | "Action": [ 1004 | "logs:CreateLogGroup", 1005 | "logs:CreateLogStream", 1006 | "logs:PutLogEvents" 1007 | ], 1008 | "Effect": "Allow", 1009 | "Resource": "arn:aws:logs:*:*:*:*" 1010 | }, 1011 | { 1012 | "Sid": "PrefixTableStream", 1013 | "Action": [ 1014 | "dynamodb:GetRecords", 1015 | "dynamodb:GetShardIterator", 1016 | "dynamodb:DescribeStream", 1017 | "dynamodb:ListStreams" 1018 | ], 1019 | "Effect": "Allow", 1020 | "Resource": { "Fn::Join": [ "", [ "arn:aws:dynamodb:", {"Ref": "AWS::Region"}, ":", { "Ref": "AWS::AccountId" }, ":table/", { "Ref": "PrefixTableName" }, "/stream/*" ] ] } 1021 | }, 1022 | { 1023 | "Sid": "PutCreateEvents", 1024 | "Action": [ 1025 | "events:PutEvents" 1026 | ], 1027 | "Effect": "Allow", 1028 | "Resource": "*" 1029 | }, 1030 | { 1031 | "Sid": "ListTables", 1032 | "Action": [ 1033 | "dynamodb:ListTables" 1034 | ], 1035 | "Effect": "Allow", 1036 | "Resource": "*" 1037 | } 1038 | ] 1039 | } 1040 | } 1041 | ] 1042 | } 1043 | }, 1044 | "WatcherPrefixInitFunction": { 1045 | "Type": "AWS::Lambda::Function", 1046 | "Properties": { 1047 | "FunctionName": "Replication-Watcher-Prefix-Init", 1048 | "Description": "When a new prefix is added to the list, start replication for all existing matching tables", 1049 | "Role": { "Fn::GetAtt": [ "WatcherPrefixInitRole", "Arn" ] }, 1050 | "Runtime": "nodejs10.x", 1051 | "Handler": "index.handler", 1052 | "MemorySize": 128, 1053 | "Timeout": 300, 1054 | "Code": { 1055 | "ZipFile": { 1056 | "Fn::Include": { 1057 | "type": "literal", 1058 | "location": "../lambda/watcher/prefix/init.min.js" 1059 | } 1060 | } 1061 | } 1062 | } 1063 | }, 1064 | "WatcherPrefixInitEvent": { 1065 | "Type": "AWS::Lambda::EventSourceMapping", 1066 | "Properties": { 1067 | "FunctionName": { "Fn::GetAtt": [ "WatcherPrefixInitFunction", "Arn" ] }, 1068 | "EventSourceArn": { "Fn::GetAtt": [ "PrefixTable", "StreamArn" ] }, 1069 | "BatchSize": 1, 1070 | "StartingPosition": "TRIM_HORIZON" 1071 | } 1072 | }, 1073 | "WatcherAlarmTopic": { 1074 | "Type": "AWS::SNS::Topic", 1075 | "Properties": { 1076 | "DisplayName": "ReplicationAlarmTopic", 1077 | "TopicName": "ReplicationAlarmTopic", 1078 | "Subscription": { 1079 | "Fn::If": [ 1080 | "NotifyOnDelay", 1081 | [ 1082 | { 1083 | "Endpoint": { "Ref": "DelayEndpoint" }, 1084 | "Protocol": { "Ref": "DelayEndpointProtocol" } 1085 | } 1086 | ], 1087 | { 1088 | "Ref": "AWS::NoValue" 1089 | } 1090 | ] 1091 | } 1092 | } 1093 | }, 1094 | "WatcherAlarmCreateRole": { 1095 | "Type": "AWS::IAM::Role", 1096 | "Properties": { 1097 | "Path": "/replication/watcher/alarm/create/", 1098 | "AssumeRolePolicyDocument" : { 1099 | "Version": "2012-10-17", 1100 | "Statement": [{ 1101 | "Effect": "Allow", 1102 | "Principal": {"Service": ["lambda.amazonaws.com"]}, 1103 | "Action": ["sts:AssumeRole"] 1104 | }] 1105 | }, 1106 | "Policies": [ 1107 | { 1108 | "PolicyName": "ReplicationWatcherAlarmCreatePolicy", 1109 | "PolicyDocument": { 1110 | "Version": "2012-10-17", 1111 | "Statement": [ 1112 | { 1113 | "Sid": "LambdaLogging", 1114 | "Action": [ 1115 | "logs:CreateLogGroup", 1116 | "logs:CreateLogStream", 1117 | "logs:PutLogEvents" 1118 | ], 1119 | "Effect": "Allow", 1120 | "Resource": "arn:aws:logs:*:*:*:*" 1121 | }, 1122 | { 1123 | "Sid": "CreateAlarm", 1124 | "Action": [ 1125 | "cloudwatch:PutMetricAlarm", 1126 | "cloudwatch:PutMetricData" 1127 | ], 1128 | "Effect": "Allow", 1129 | "Resource": "*" 1130 | } 1131 | ] 1132 | } 1133 | } 1134 | ] 1135 | } 1136 | }, 1137 | "WatcherAlarmCreateFunction": { 1138 | "Type": "AWS::Lambda::Function", 1139 | "Properties": { 1140 | "FunctionName": "Replication-Watcher-Alarm-Create", 1141 | "Description": "Creates an alarm for a replication's MinutesBehindRecord metric", 1142 | "Role": { "Fn::GetAtt": [ "WatcherAlarmCreateRole", "Arn" ] }, 1143 | "Runtime": "nodejs10.x", 1144 | "Handler": "index.handler", 1145 | "MemorySize": 128, 1146 | "Timeout": 300, 1147 | "Code": { 1148 | "ZipFile": { 1149 | "Fn::Include": { 1150 | "type": "literal", 1151 | "location": "../lambda/watcher/alarm/create.min.js", 1152 | "context": { 1153 | "snsTopic": { "Ref": "WatcherAlarmTopic" }, 1154 | "delayThreshold": { "Ref": "DelayThreshold" } 1155 | } 1156 | } 1157 | } 1158 | } 1159 | } 1160 | }, 1161 | "WatcherAlarmCreateRule": { 1162 | "Type": "AWS::Events::Rule", 1163 | "Properties": { 1164 | "Description": "Invoke the replication watcher alarm create function when any event source is added to the replicator", 1165 | "Name": "Replication-Watcher-Alarm-Create", 1166 | "State": "ENABLED", 1167 | "EventPattern": { 1168 | "detail-type": [ 1169 | "AWS API Call via CloudTrail" 1170 | ], 1171 | "detail": { 1172 | "eventSource": [ 1173 | "lambda.amazonaws.com" 1174 | ], 1175 | "eventName": [ 1176 | "CreateEventSourceMapping20150331" 1177 | ], 1178 | "responseElements": { 1179 | "functionArn": [ 1180 | { "Fn::GetAtt": [ "ReplicatorFunction", "Arn" ] } 1181 | ] 1182 | } 1183 | } 1184 | }, 1185 | "Targets": [ 1186 | { 1187 | "Arn": { "Fn::GetAtt": [ "WatcherAlarmCreateFunction", "Arn" ] }, 1188 | "Id": "Replication-Watcher-Alarm-Create" 1189 | } 1190 | ] 1191 | } 1192 | }, 1193 | "WatcherAlarmCreatePermission": { 1194 | "Type": "AWS::Lambda::Permission", 1195 | "Properties": { 1196 | "Action": "lambda:InvokeFunction", 1197 | "FunctionName": { "Fn::GetAtt": [ "WatcherAlarmCreateFunction", "Arn" ] }, 1198 | "Principal": "events.amazonaws.com", 1199 | "SourceArn": { "Fn::GetAtt": [ "WatcherAlarmCreateRule", "Arn" ] } 1200 | } 1201 | }, 1202 | "WatcherAlarmDeleteRole": { 1203 | "Type": "AWS::IAM::Role", 1204 | "Properties": { 1205 | "Path": "/replication/watcher/alarm/delete/", 1206 | "AssumeRolePolicyDocument" : { 1207 | "Version": "2012-10-17", 1208 | "Statement": [{ 1209 | "Effect": "Allow", 1210 | "Principal": {"Service": ["lambda.amazonaws.com"]}, 1211 | "Action": ["sts:AssumeRole"] 1212 | }] 1213 | }, 1214 | "Policies": [ 1215 | { 1216 | "PolicyName": "ReplicationWatcherAlarmDeletePolicy", 1217 | "PolicyDocument": { 1218 | "Version": "2012-10-17", 1219 | "Statement": [ 1220 | { 1221 | "Sid": "LambdaLogging", 1222 | "Action": [ 1223 | "logs:CreateLogGroup", 1224 | "logs:CreateLogStream", 1225 | "logs:PutLogEvents" 1226 | ], 1227 | "Effect": "Allow", 1228 | "Resource": "arn:aws:logs:*:*:*:*" 1229 | }, 1230 | { 1231 | "Sid": "DeleteAlarm", 1232 | "Action": [ 1233 | "cloudwatch:DeleteAlarms" 1234 | ], 1235 | "Effect": "Allow", 1236 | "Resource": "*" 1237 | } 1238 | ] 1239 | } 1240 | } 1241 | ] 1242 | } 1243 | }, 1244 | "WatcherAlarmDeleteFunction": { 1245 | "Type": "AWS::Lambda::Function", 1246 | "Properties": { 1247 | "FunctionName": "Replication-Watcher-Alarm-Delete", 1248 | "Description": "Deletes a replication's MinutsBehindRecord alarm", 1249 | "Role": { "Fn::GetAtt": [ "WatcherAlarmDeleteRole", "Arn" ] }, 1250 | "Runtime": "nodejs10.x", 1251 | "Handler": "index.handler", 1252 | "MemorySize": 128, 1253 | "Timeout": 300, 1254 | "Code": { 1255 | "ZipFile": { 1256 | "Fn::Include": { 1257 | "type": "literal", 1258 | "location": "../lambda/watcher/alarm/delete.min.js" 1259 | } 1260 | } 1261 | } 1262 | } 1263 | }, 1264 | "WatcherAlarmDeleteRule": { 1265 | "Type": "AWS::Events::Rule", 1266 | "Properties": { 1267 | "Description": "Invoke the replication watcher alarm delete function when any event source is removed from the replicator", 1268 | "Name": "Replication-Watcher-Alarm-Delete", 1269 | "State": "ENABLED", 1270 | "EventPattern": { 1271 | "detail-type": [ 1272 | "AWS API Call via CloudTrail" 1273 | ], 1274 | "detail": { 1275 | "eventSource": [ 1276 | "lambda.amazonaws.com" 1277 | ], 1278 | "eventName": [ 1279 | "DeleteEventSourceMapping20150331" 1280 | ], 1281 | "responseElements": { 1282 | "functionArn": [ 1283 | { "Fn::GetAtt": [ "ReplicatorFunction", "Arn" ] } 1284 | ] 1285 | } 1286 | } 1287 | }, 1288 | "Targets": [ 1289 | { 1290 | "Arn": { "Fn::GetAtt": [ "WatcherAlarmDeleteFunction", "Arn" ] }, 1291 | "Id": "Replication-Watcher-Alarm-Delete" 1292 | } 1293 | ] 1294 | } 1295 | }, 1296 | "WatcherAlarmDeletePermission": { 1297 | "Type": "AWS::Lambda::Permission", 1298 | "Properties": { 1299 | "Action": "lambda:InvokeFunction", 1300 | "FunctionName": { "Fn::GetAtt": [ "WatcherAlarmDeleteFunction", "Arn" ] }, 1301 | "Principal": "events.amazonaws.com", 1302 | "SourceArn": { "Fn::GetAtt": [ "WatcherAlarmDeleteRule", "Arn" ] } 1303 | } 1304 | }, 1305 | "ReplicationApi": { 1306 | "Type": "AWS::ApiGateway::RestApi", 1307 | "Properties": { 1308 | "Name": { "Ref": "ApiName" }, 1309 | "Description": "DynamoDB Replication Api" 1310 | } 1311 | }, 1312 | "ApiDeployment": { 1313 | "Type": "AWS::ApiGateway::Deployment", 1314 | "DependsOn": [ 1315 | "ApiReplicationsPostMethod", "ApiReplicationsIdentifierDeleteMethod", "ApiReplicationsGetMethod", 1316 | "ApiPrefixesPostMethod", "ApiPrefixesIdentifierDeleteMethod", "ApiPrefixesGetMethod", 1317 | "ApiMetricsNamespaceMetricIdentifierGetMethod" 1318 | ], 1319 | "Properties": { 1320 | "RestApiId": { "Ref": "ReplicationApi"}, 1321 | "Description": "FLUFF", 1322 | "StageName": "DEPLOY" 1323 | } 1324 | }, 1325 | "ApiStage": { 1326 | "Type": "AWS::ApiGateway::Stage", 1327 | "Properties": { 1328 | "StageName": "PROD", 1329 | "Description": "PROD", 1330 | "RestApiId": { "Ref": "ReplicationApi" }, 1331 | "DeploymentId": { "Ref": "ApiDeployment" }, 1332 | "Variables": { 1333 | "CONTROLLER_TABLE": { "Ref": "ControllerTableName" }, 1334 | "PREFIX_TABLE": { "Ref": "PrefixTableName" } 1335 | } 1336 | } 1337 | }, 1338 | "ApiKey": { 1339 | "Type": "AWS::ApiGateway::ApiKey", 1340 | "DependsOn": [ "ApiDeployment", "ApiStage" ], 1341 | "Properties": { 1342 | "Name": "ReplicationConsoleApiKey", 1343 | "Description": "fluff", 1344 | "Enabled": true, 1345 | "StageKeys": [ 1346 | { 1347 | "RestApiId": { "Ref": "ReplicationApi" }, 1348 | "StageName": { "Ref": "ApiStage" } 1349 | } 1350 | ] 1351 | } 1352 | }, 1353 | "ApiReplicationsResource": { 1354 | "Type": "AWS::ApiGateway::Resource", 1355 | "Properties": { 1356 | "RestApiId": {"Ref": "ReplicationApi" }, 1357 | "ParentId": { "Fn::GetAtt": [ "ReplicationApi", "RootResourceId" ] }, 1358 | "PathPart": "replications" 1359 | } 1360 | }, 1361 | "ApiReplicationsIdentifierResource": { 1362 | "Type": "AWS::ApiGateway::Resource", 1363 | "Properties": { 1364 | "RestApiId": { "Ref": "ReplicationApi" }, 1365 | "ParentId": { "Ref": "ApiReplicationsResource" }, 1366 | "PathPart": "{replication}" 1367 | } 1368 | }, 1369 | "ApiPrefixesResource": { 1370 | "Type": "AWS::ApiGateway::Resource", 1371 | "Properties": { 1372 | "RestApiId": { "Ref": "ReplicationApi" }, 1373 | "ParentId": { "Fn::GetAtt": [ "ReplicationApi", "RootResourceId" ] }, 1374 | "PathPart": "prefixes" 1375 | } 1376 | }, 1377 | "ApiPrefixesIdentifierResource": { 1378 | "Type": "AWS::ApiGateway::Resource", 1379 | "Properties": { 1380 | "RestApiId": { "Ref": "ReplicationApi" }, 1381 | "ParentId": { "Ref": "ApiPrefixesResource" }, 1382 | "PathPart": "{prefix}" 1383 | } 1384 | }, 1385 | "ApiMetricsResource": { 1386 | "Type": "AWS::ApiGateway::Resource", 1387 | "Properties": { 1388 | "RestApiId": { "Ref": "ReplicationApi" }, 1389 | "ParentId": { "Fn::GetAtt": [ "ReplicationApi", "RootResourceId" ] }, 1390 | "PathPart": "metrics" 1391 | } 1392 | }, 1393 | "ApiMetricsNamespaceIdentifierResource": { 1394 | "Type": "AWS::ApiGateway::Resource", 1395 | "Properties": { 1396 | "RestApiId": { "Ref": "ReplicationApi" }, 1397 | "ParentId": { "Ref": "ApiMetricsResource" }, 1398 | "PathPart": "{namespace}" 1399 | } 1400 | }, 1401 | "ApiMetricsNamespaceMetricIdentifierResource": { 1402 | "Type": "AWS::ApiGateway::Resource", 1403 | "Properties": { 1404 | "RestApiId": { "Ref": "ReplicationApi" }, 1405 | "ParentId": { "Ref": "ApiMetricsNamespaceIdentifierResource" }, 1406 | "PathPart": "{metric}" 1407 | } 1408 | }, 1409 | "ApiReplicationsPostMethod": { 1410 | "Type": "AWS::ApiGateway::Method", 1411 | "Properties": { 1412 | "RestApiId": { "Ref": "ReplicationApi" }, 1413 | "ResourceId": { "Ref": "ApiReplicationsResource" }, 1414 | "ApiKeyRequired": true, 1415 | "AuthorizationType": "NONE", 1416 | "HttpMethod": "POST", 1417 | "Integration": { 1418 | "Type": "AWS", 1419 | "Uri": { "Fn::Join": [ "", [ "arn:aws:apigateway:", { "Ref": "AWS::Region" }, ":lambda:path/2015-03-31/functions/", { "Fn::GetAtt": [ "ApiReplicationsPostFunction", "Arn" ] }, "/invocations" ] ] }, 1420 | "IntegrationHttpMethod": "POST", 1421 | "RequestTemplates": { 1422 | "application/json": "{\n \"table\": $input.json('$.key'),\n \"controller\": \"$util.escapeJavaScript($stageVariables.get('CONTROLLER_TABLE'))\"\n}" 1423 | }, 1424 | "IntegrationResponses": [ 1425 | { 1426 | "StatusCode": "200", 1427 | "ResponseParameters": { 1428 | "method.response.header.Access-Control-Allow-Origin": "'*'" 1429 | }, 1430 | "ResponseTemplates": { 1431 | "application/json": "" 1432 | } 1433 | } 1434 | ] 1435 | }, 1436 | "MethodResponses": [ 1437 | { 1438 | "StatusCode": "200", 1439 | "ResponseModels": { 1440 | "application/json": "Empty" 1441 | }, 1442 | "ResponseParameters": { 1443 | "method.response.header.Access-Control-Allow-Origin": true 1444 | } 1445 | } 1446 | ] 1447 | } 1448 | }, 1449 | "ApiReplicationsGetMethod": { 1450 | "Type": "AWS::ApiGateway::Method", 1451 | "Properties": { 1452 | "RestApiId": { "Ref": "ReplicationApi" }, 1453 | "ResourceId": { "Ref": "ApiReplicationsResource" }, 1454 | "ApiKeyRequired": true, 1455 | "AuthorizationType": "NONE", 1456 | "HttpMethod": "GET", 1457 | "Integration": { 1458 | "Type": "AWS", 1459 | "Uri": { "Fn::Join": [ "", [ "arn:aws:apigateway:", { "Ref": "AWS::Region" }, ":lambda:path/2015-03-31/functions/", { "Fn::GetAtt": [ "ApiTableGetFunction", "Arn" ] }, "/invocations" ] ] }, 1460 | "IntegrationHttpMethod": "POST", 1461 | "RequestTemplates": { 1462 | "application/json": "{\n \"table\": \"$util.escapeJavaScript($stageVariables.get('CONTROLLER_TABLE'))\"\n}" 1463 | }, 1464 | "IntegrationResponses": [ 1465 | { 1466 | "StatusCode": "200", 1467 | "ResponseParameters": { 1468 | "method.response.header.Access-Control-Allow-Origin": "'*'" 1469 | }, 1470 | "ResponseTemplates": { 1471 | "application/json": "" 1472 | } 1473 | } 1474 | ] 1475 | }, 1476 | "MethodResponses": [ 1477 | { 1478 | "StatusCode": "200", 1479 | "ResponseModels": { 1480 | "application/json": "Empty" 1481 | }, 1482 | "ResponseParameters": { 1483 | "method.response.header.Access-Control-Allow-Origin": true 1484 | } 1485 | } 1486 | ] 1487 | } 1488 | }, 1489 | "ApiReplicationsCors": { 1490 | "Type": "AWS::ApiGateway::Method", 1491 | "Properties": { 1492 | "RestApiId": { "Ref": "ReplicationApi" }, 1493 | "ResourceId": { "Ref": "ApiReplicationsResource" }, 1494 | "AuthorizationType": "NONE", 1495 | "HttpMethod": "OPTIONS", 1496 | "Integration": { 1497 | "Type": "MOCK", 1498 | "RequestTemplates": { 1499 | "application/json": "{\n \"statusCode\": 200\n}" 1500 | }, 1501 | "IntegrationResponses": [ 1502 | { 1503 | "StatusCode": "200", 1504 | "ResponseParameters": { 1505 | "method.response.header.Access-Control-Allow-Headers": "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token'", 1506 | "method.response.header.Access-Control-Allow-Methods": "'POST,GET,OPTIONS'", 1507 | "method.response.header.Access-Control-Allow-Origin": "'*'" 1508 | }, 1509 | "ResponseTemplates": { 1510 | "application/json": "" 1511 | } 1512 | } 1513 | ] 1514 | }, 1515 | "MethodResponses": [ 1516 | { 1517 | "StatusCode": "200", 1518 | "ResponseModels": { 1519 | "application/json": "Empty" 1520 | }, 1521 | "ResponseParameters": { 1522 | "method.response.header.Access-Control-Allow-Headers": true, 1523 | "method.response.header.Access-Control-Allow-Methods": true, 1524 | "method.response.header.Access-Control-Allow-Origin": true 1525 | } 1526 | } 1527 | ] 1528 | } 1529 | }, 1530 | "ApiReplicationsIdentifierDeleteMethod": { 1531 | "Type": "AWS::ApiGateway::Method", 1532 | "Properties": { 1533 | "RestApiId": { "Ref": "ReplicationApi" }, 1534 | "ResourceId": { "Ref": "ApiReplicationsIdentifierResource" }, 1535 | "ApiKeyRequired": true, 1536 | "AuthorizationType": "NONE", 1537 | "HttpMethod": "DELETE", 1538 | "Integration": { 1539 | "Type": "AWS", 1540 | "Uri": { "Fn::Join": [ "", [ "arn:aws:apigateway:", { "Ref": "AWS::Region" }, ":lambda:path/2015-03-31/functions/", { "Fn::GetAtt": [ "ApiReplicationsDeleteFunction", "Arn" ] }, "/invocations" ] ] }, 1541 | "IntegrationHttpMethod": "POST", 1542 | "RequestTemplates": { 1543 | "application/json": "{\n \"table\": \"$input.params('replication')\",\n \"controller\": \"$util.escapeJavaScript($stageVariables.get('CONTROLLER_TABLE'))\"\n}" 1544 | }, 1545 | "IntegrationResponses": [ 1546 | { 1547 | "StatusCode": "200", 1548 | "ResponseParameters": { 1549 | "method.response.header.Access-Control-Allow-Origin": "'*'" 1550 | }, 1551 | "ResponseTemplates": { 1552 | "application/json": "" 1553 | } 1554 | } 1555 | ] 1556 | }, 1557 | "MethodResponses": [ 1558 | { 1559 | "StatusCode": "200", 1560 | "ResponseModels": { 1561 | "application/json": "Empty" 1562 | }, 1563 | "ResponseParameters": { 1564 | "method.response.header.Access-Control-Allow-Origin": true 1565 | } 1566 | } 1567 | ] 1568 | } 1569 | }, 1570 | "ApiReplicationsIdentifierCors": { 1571 | "Type": "AWS::ApiGateway::Method", 1572 | "Properties": { 1573 | "RestApiId": { "Ref": "ReplicationApi" }, 1574 | "ResourceId": { "Ref": "ApiReplicationsIdentifierResource" }, 1575 | "AuthorizationType": "NONE", 1576 | "HttpMethod": "OPTIONS", 1577 | "Integration": { 1578 | "Type": "MOCK", 1579 | "RequestTemplates": { 1580 | "application/json": "{\n \"statusCode\": 200\n}" 1581 | }, 1582 | "IntegrationResponses": [ 1583 | { 1584 | "StatusCode": "200", 1585 | "ResponseParameters": { 1586 | "method.response.header.Access-Control-Allow-Headers": "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token'", 1587 | "method.response.header.Access-Control-Allow-Methods": "'DELETE,OPTIONS'", 1588 | "method.response.header.Access-Control-Allow-Origin": "'*'" 1589 | }, 1590 | "ResponseTemplates": { 1591 | "application/json": "" 1592 | } 1593 | } 1594 | ] 1595 | }, 1596 | "MethodResponses": [ 1597 | { 1598 | "StatusCode": "200", 1599 | "ResponseModels": { 1600 | "application/json": "Empty" 1601 | }, 1602 | "ResponseParameters": { 1603 | "method.response.header.Access-Control-Allow-Headers": true, 1604 | "method.response.header.Access-Control-Allow-Methods": true, 1605 | "method.response.header.Access-Control-Allow-Origin": true 1606 | } 1607 | } 1608 | ] 1609 | } 1610 | }, 1611 | "ApiPrefixesPostMethod": { 1612 | "Type": "AWS::ApiGateway::Method", 1613 | "Properties": { 1614 | "RestApiId": { "Ref": "ReplicationApi" }, 1615 | "ResourceId": { "Ref": "ApiPrefixesResource" }, 1616 | "ApiKeyRequired": true, 1617 | "AuthorizationType": "NONE", 1618 | "HttpMethod": "POST", 1619 | "Integration": { 1620 | "Type": "AWS", 1621 | "Uri": { "Fn::Join": [ "", [ "arn:aws:apigateway:", { "Ref": "AWS::Region" }, ":lambda:path/2015-03-31/functions/", { "Fn::GetAtt": [ "ApiPrefixesPostFunction", "Arn" ] }, "/invocations" ] ] }, 1622 | "IntegrationHttpMethod": "POST", 1623 | "RequestTemplates": { 1624 | "application/json": "{\n \"prefix\": $input.json('$.key'),\n \"prefixTable\": \"$util.escapeJavaScript($stageVariables.get('PREFIX_TABLE'))\"\n}" 1625 | }, 1626 | "IntegrationResponses": [ 1627 | { 1628 | "StatusCode": "200", 1629 | "ResponseParameters": { 1630 | "method.response.header.Access-Control-Allow-Origin": "'*'" 1631 | }, 1632 | "ResponseTemplates": { 1633 | "application/json": "" 1634 | } 1635 | } 1636 | ] 1637 | }, 1638 | "MethodResponses": [ 1639 | { 1640 | "StatusCode": "200", 1641 | "ResponseModels": { 1642 | "application/json": "Empty" 1643 | }, 1644 | "ResponseParameters": { 1645 | "method.response.header.Access-Control-Allow-Origin": true 1646 | } 1647 | } 1648 | ] 1649 | } 1650 | }, 1651 | "ApiPrefixesGetMethod": { 1652 | "Type": "AWS::ApiGateway::Method", 1653 | "Properties": { 1654 | "RestApiId": { "Ref": "ReplicationApi" }, 1655 | "ResourceId": { "Ref": "ApiPrefixesResource" }, 1656 | "ApiKeyRequired": true, 1657 | "AuthorizationType": "NONE", 1658 | "HttpMethod": "GET", 1659 | "Integration": { 1660 | "Type": "AWS", 1661 | "Uri": { "Fn::Join": [ "", [ "arn:aws:apigateway:", { "Ref": "AWS::Region" }, ":lambda:path/2015-03-31/functions/", { "Fn::GetAtt": [ "ApiTableGetFunction", "Arn" ] }, "/invocations" ] ] }, 1662 | "IntegrationHttpMethod": "POST", 1663 | "RequestTemplates": { 1664 | "application/json": "{\n \"table\": \"$util.escapeJavaScript($stageVariables.get('PREFIX_TABLE'))\"\n}" 1665 | }, 1666 | "IntegrationResponses": [ 1667 | { 1668 | "StatusCode": "200", 1669 | "ResponseParameters": { 1670 | "method.response.header.Access-Control-Allow-Origin": "'*'" 1671 | }, 1672 | "ResponseTemplates": { 1673 | "application/json": "" 1674 | } 1675 | } 1676 | ] 1677 | }, 1678 | "MethodResponses": [ 1679 | { 1680 | "StatusCode": "200", 1681 | "ResponseModels": { 1682 | "application/json": "Empty" 1683 | }, 1684 | "ResponseParameters": { 1685 | "method.response.header.Access-Control-Allow-Origin": true 1686 | } 1687 | } 1688 | ] 1689 | } 1690 | }, 1691 | "ApiPrefixesCors": { 1692 | "Type": "AWS::ApiGateway::Method", 1693 | "Properties": { 1694 | "RestApiId": { "Ref": "ReplicationApi" }, 1695 | "ResourceId": { "Ref": "ApiPrefixesResource" }, 1696 | "AuthorizationType": "NONE", 1697 | "HttpMethod": "OPTIONS", 1698 | "Integration": { 1699 | "Type": "MOCK", 1700 | "RequestTemplates": { 1701 | "application/json": "{\n \"statusCode\": 200\n}" 1702 | }, 1703 | "IntegrationResponses": [ 1704 | { 1705 | "StatusCode": "200", 1706 | "ResponseParameters": { 1707 | "method.response.header.Access-Control-Allow-Headers": "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token'", 1708 | "method.response.header.Access-Control-Allow-Methods": "'POST,GET,OPTIONS'", 1709 | "method.response.header.Access-Control-Allow-Origin": "'*'" 1710 | }, 1711 | "ResponseTemplates": { 1712 | "application/json": "" 1713 | } 1714 | } 1715 | ] 1716 | }, 1717 | "MethodResponses": [ 1718 | { 1719 | "StatusCode": "200", 1720 | "ResponseModels": { 1721 | "application/json": "Empty" 1722 | }, 1723 | "ResponseParameters": { 1724 | "method.response.header.Access-Control-Allow-Headers": true, 1725 | "method.response.header.Access-Control-Allow-Methods": true, 1726 | "method.response.header.Access-Control-Allow-Origin": true 1727 | } 1728 | } 1729 | ] 1730 | } 1731 | }, 1732 | "ApiPrefixesIdentifierDeleteMethod": { 1733 | "Type": "AWS::ApiGateway::Method", 1734 | "Properties": { 1735 | "RestApiId": { "Ref": "ReplicationApi" }, 1736 | "ResourceId": { "Ref": "ApiPrefixesIdentifierResource" }, 1737 | "ApiKeyRequired": true, 1738 | "AuthorizationType": "NONE", 1739 | "HttpMethod": "DELETE", 1740 | "Integration": { 1741 | "Type": "AWS", 1742 | "Uri": { "Fn::Join": [ "", [ "arn:aws:apigateway:", { "Ref": "AWS::Region" }, ":lambda:path/2015-03-31/functions/", { "Fn::GetAtt": [ "ApiPrefixesDeleteFunction", "Arn" ] }, "/invocations" ] ] }, 1743 | "IntegrationHttpMethod": "POST", 1744 | "RequestTemplates": { 1745 | "application/json": "{\n \"prefix\": \"$input.params('prefix')\",\n \"prefixTable\": \"$util.escapeJavaScript($stageVariables.get('PREFIX_TABLE'))\"\n}" 1746 | }, 1747 | "IntegrationResponses": [ 1748 | { 1749 | "StatusCode": "200", 1750 | "ResponseParameters": { 1751 | "method.response.header.Access-Control-Allow-Origin": "'*'" 1752 | }, 1753 | "ResponseTemplates": { 1754 | "application/json": "" 1755 | } 1756 | } 1757 | ] 1758 | }, 1759 | "MethodResponses": [ 1760 | { 1761 | "StatusCode": "200", 1762 | "ResponseModels": { 1763 | "application/json": "Empty" 1764 | }, 1765 | "ResponseParameters": { 1766 | "method.response.header.Access-Control-Allow-Origin": true 1767 | } 1768 | } 1769 | ] 1770 | } 1771 | }, 1772 | "ApiPrefixesIdentifierCors": { 1773 | "Type": "AWS::ApiGateway::Method", 1774 | "Properties": { 1775 | "RestApiId": { "Ref": "ReplicationApi" }, 1776 | "ResourceId": { "Ref": "ApiPrefixesIdentifierResource" }, 1777 | "AuthorizationType": "NONE", 1778 | "HttpMethod": "OPTIONS", 1779 | "Integration": { 1780 | "Type": "MOCK", 1781 | "RequestTemplates": { 1782 | "application/json": "{\n \"statusCode\": 200\n}" 1783 | }, 1784 | "IntegrationResponses": [ 1785 | { 1786 | "StatusCode": "200", 1787 | "ResponseParameters": { 1788 | "method.response.header.Access-Control-Allow-Headers": "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token'", 1789 | "method.response.header.Access-Control-Allow-Methods": "'DELETE,OPTIONS'", 1790 | "method.response.header.Access-Control-Allow-Origin": "'*'" 1791 | }, 1792 | "ResponseTemplates": { 1793 | "application/json": "" 1794 | } 1795 | } 1796 | ] 1797 | }, 1798 | "MethodResponses": [ 1799 | { 1800 | "StatusCode": "200", 1801 | "ResponseModels": { 1802 | "application/json": "Empty" 1803 | }, 1804 | "ResponseParameters": { 1805 | "method.response.header.Access-Control-Allow-Headers": true, 1806 | "method.response.header.Access-Control-Allow-Methods": true, 1807 | "method.response.header.Access-Control-Allow-Origin": true 1808 | } 1809 | } 1810 | ] 1811 | } 1812 | }, 1813 | "ApiMetricsNamespaceMetricIdentifierGetMethod": { 1814 | "Type": "AWS::ApiGateway::Method", 1815 | "Properties": { 1816 | "RestApiId": { "Ref": "ReplicationApi" }, 1817 | "ResourceId": { "Ref": "ApiMetricsNamespaceMetricIdentifierResource" }, 1818 | "ApiKeyRequired": true, 1819 | "AuthorizationType": "NONE", 1820 | "HttpMethod": "GET", 1821 | "RequestParameters": { 1822 | "method.request.querystring.dimension": false, 1823 | "method.request.querystring.count": true, 1824 | "method.request.querystring.period": true, 1825 | "method.request.querystring.unit": true, 1826 | "method.request.querystring.statistic": true 1827 | }, 1828 | "Integration": { 1829 | "Type": "AWS", 1830 | "Uri": { "Fn::Join": [ "", [ "arn:aws:apigateway:", { "Ref": "AWS::Region" }, ":lambda:path/2015-03-31/functions/", { "Fn::GetAtt": [ "ApiMetricsGetFunction", "Arn" ] }, "/invocations" ] ] }, 1831 | "IntegrationHttpMethod": "POST", 1832 | "RequestParameters": { 1833 | "integration.request.querystring.dimension": "method.request.querystring.dimension", 1834 | "integration.request.querystring.count": "method.request.querystring.count", 1835 | "integration.request.querystring.period": "method.request.querystring.period", 1836 | "integration.request.querystring.unit": "method.request.querystring.unit", 1837 | "integration.request.querystring.statistic": "method.request.querystring.statistic" 1838 | }, 1839 | "RequestTemplates": { 1840 | "application/json": { "Fn::Join": [ "\n", [ 1841 | "{", 1842 | " \"namespace\": \"$input.params('namespace')\",", 1843 | " \"metric\": \"$input.params('metric')\",", 1844 | " \"dimension\": \"$input.params('dimension')\",", 1845 | " \"count\": \"$input.params('count')\",", 1846 | " \"period\": \"$input.params('period')\",", 1847 | " \"unit\": \"$input.params('unit')\",", 1848 | " \"statistic\": \"$input.params('statistic')\"", 1849 | "}" 1850 | ]]} 1851 | }, 1852 | "IntegrationResponses": [ 1853 | { 1854 | "StatusCode": "200", 1855 | "ResponseParameters": { 1856 | "method.response.header.Access-Control-Allow-Origin": "'*'" 1857 | }, 1858 | "ResponseTemplates": { 1859 | "application/json": "" 1860 | } 1861 | } 1862 | ] 1863 | }, 1864 | "MethodResponses": [ 1865 | { 1866 | "StatusCode": "200", 1867 | "ResponseModels": { 1868 | "application/json": "Empty" 1869 | }, 1870 | "ResponseParameters": { 1871 | "method.response.header.Access-Control-Allow-Origin": true 1872 | } 1873 | } 1874 | ] 1875 | } 1876 | }, 1877 | "ApiMetricsNamespaceMetricIdentifierCors": { 1878 | "Type": "AWS::ApiGateway::Method", 1879 | "Properties": { 1880 | "RestApiId": { "Ref": "ReplicationApi" }, 1881 | "ResourceId": { "Ref": "ApiMetricsNamespaceMetricIdentifierResource" }, 1882 | "AuthorizationType": "NONE", 1883 | "HttpMethod": "OPTIONS", 1884 | "Integration": { 1885 | "Type": "MOCK", 1886 | "RequestTemplates": { 1887 | "application/json": "{\n \"statusCode\": 200\n}" 1888 | }, 1889 | "IntegrationResponses": [ 1890 | { 1891 | "StatusCode": "200", 1892 | "ResponseParameters": { 1893 | "method.response.header.Access-Control-Allow-Headers": "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token'", 1894 | "method.response.header.Access-Control-Allow-Methods": "'GET,OPTIONS'", 1895 | "method.response.header.Access-Control-Allow-Origin": "'*'" 1896 | }, 1897 | "ResponseTemplates": { 1898 | "application/json": "" 1899 | } 1900 | } 1901 | ] 1902 | }, 1903 | "MethodResponses": [ 1904 | { 1905 | "StatusCode": "200", 1906 | "ResponseModels": { 1907 | "application/json": "Empty" 1908 | }, 1909 | "ResponseParameters": { 1910 | "method.response.header.Access-Control-Allow-Headers": true, 1911 | "method.response.header.Access-Control-Allow-Methods": true, 1912 | "method.response.header.Access-Control-Allow-Origin": true 1913 | } 1914 | } 1915 | ] 1916 | } 1917 | }, 1918 | "ApiTableGetRole": { 1919 | "Type": "AWS::IAM::Role", 1920 | "Properties": { 1921 | "Path": "/replication/api/table/get/", 1922 | "AssumeRolePolicyDocument": { 1923 | "Version": "2012-10-17", 1924 | "Statement": [ 1925 | { 1926 | "Effect": "Allow", 1927 | "Principal": { 1928 | "Service": [ 1929 | "lambda.amazonaws.com" 1930 | ] 1931 | }, 1932 | "Action": [ 1933 | "sts:AssumeRole" 1934 | ] 1935 | } 1936 | ] 1937 | }, 1938 | "Policies": [ 1939 | { 1940 | "PolicyName": "ApiTableGetPolicy", 1941 | "PolicyDocument": { 1942 | "Version": "2012-10-17", 1943 | "Statement": [ 1944 | { 1945 | "Sid": "LambdaLogging", 1946 | "Effect": "Allow", 1947 | "Action": [ 1948 | "logs:CreateLogGroup", 1949 | "logs:CreateLogStream", 1950 | "logs:PutLogEvents" 1951 | ], 1952 | "Resource": "arn:aws:logs:*:*:*" 1953 | }, 1954 | { 1955 | "Sid": "ScanTables", 1956 | "Effect": "Allow", 1957 | "Action": [ 1958 | "dynamodb:Scan" 1959 | ], 1960 | "Resource": { "Fn::Join": [ ":", [ "arn", "aws", "dynamodb", { "Ref": "AWS::Region" }, { "Ref": "AWS::AccountId" }, "table/*" ] ] } 1961 | } 1962 | ] 1963 | } 1964 | } 1965 | ] 1966 | } 1967 | }, 1968 | "ApiTableGetFunction": { 1969 | "Type": "AWS::Lambda::Function", 1970 | "Properties": { 1971 | "FunctionName": "Replication-API-Table-Get", 1972 | "Description": "Returns a list of all replications", 1973 | "Role": { "Fn::GetAtt": [ "ApiTableGetRole", "Arn" ] }, 1974 | "Runtime": "nodejs10.x", 1975 | "Handler": "index.handler", 1976 | "MemorySize": 128, 1977 | "Timeout": 300, 1978 | "Code": { 1979 | "ZipFile": { 1980 | "Fn::Include": { 1981 | "type": "literal", 1982 | "location": "../lambda/api/tables/get.min.js" 1983 | } 1984 | } 1985 | } 1986 | } 1987 | }, 1988 | "ApiReplicationsGetPermission": { 1989 | "Type": "AWS::Lambda::Permission", 1990 | "Properties": { 1991 | "Action": "lambda:InvokeFunction", 1992 | "FunctionName": { "Ref": "ApiTableGetFunction" }, 1993 | "Principal": "apigateway.amazonaws.com", 1994 | "SourceArn": { "Fn::Join": [ "", [ "arn:aws:execute-api:", { "Ref": "AWS::Region" }, ":", { "Ref": "AWS::AccountId"}, ":", { "Ref" : "ReplicationApi" }, "/*/GET/replications" ] ] } 1995 | } 1996 | }, 1997 | "ApiPrefixesGetPermission": { 1998 | "Type": "AWS::Lambda::Permission", 1999 | "Properties": { 2000 | "Action": "lambda:InvokeFunction", 2001 | "FunctionName": { "Ref": "ApiTableGetFunction" }, 2002 | "Principal": "apigateway.amazonaws.com", 2003 | "SourceArn": { "Fn::Join": [ "", [ "arn:aws:execute-api:", { "Ref": "AWS::Region" }, ":", { "Ref": "AWS::AccountId"}, ":", { "Ref" : "ReplicationApi" }, "/*/GET/prefixes" ] ] } 2004 | } 2005 | }, 2006 | "ApiReplicationsPostRole": { 2007 | "Type": "AWS::IAM::Role", 2008 | "Properties": { 2009 | "Path": "/replication/api/replications/post/", 2010 | "AssumeRolePolicyDocument": { 2011 | "Version": "2012-10-17", 2012 | "Statement": [ 2013 | { 2014 | "Effect": "Allow", 2015 | "Principal": { 2016 | "Service": [ 2017 | "lambda.amazonaws.com" 2018 | ] 2019 | }, 2020 | "Action": [ 2021 | "sts:AssumeRole" 2022 | ] 2023 | } 2024 | ] 2025 | }, 2026 | "Policies": [ 2027 | { 2028 | "PolicyName": "ApiReplicationsPostPolicy", 2029 | "PolicyDocument": { 2030 | "Version": "2012-10-17", 2031 | "Statement": [ 2032 | { 2033 | "Sid": "LambdaLogging", 2034 | "Effect": "Allow", 2035 | "Action": [ 2036 | "logs:CreateLogGroup", 2037 | "logs:CreateLogStream", 2038 | "logs:PutLogEvents" 2039 | ], 2040 | "Resource": "arn:aws:logs:*:*:*" 2041 | }, 2042 | { 2043 | "Sid": "CheckSource", 2044 | "Effect": "Allow", 2045 | "Action": [ 2046 | "dynamodb:DescribeTable" 2047 | ], 2048 | "Resource": { "Fn::Join": [ ":", [ "arn", "aws", "dynamodb", { "Ref": "AWS::Region" }, { "Ref": "AWS::AccountId" }, "table/*" ] ] } 2049 | }, 2050 | { 2051 | "Sid": "AddReplication", 2052 | "Action": [ 2053 | "dynamodb:PutItem" 2054 | ], 2055 | "Effect": "Allow", 2056 | "Resource": { "Fn::Join": [ "", [ "arn:aws:dynamodb:", { "Ref": "AWS::Region" }, ":", { "Ref": "AWS::AccountId" }, ":table/", { "Ref": "ControllerTableName" } ] ] } 2057 | } 2058 | ] 2059 | } 2060 | } 2061 | ] 2062 | } 2063 | }, 2064 | "ApiReplicationsPostFunction": { 2065 | "Type": "AWS::Lambda::Function", 2066 | "Properties": { 2067 | "FunctionName": "Replication-API-Replications-Post", 2068 | "Description": "Adds a replication to the controller table", 2069 | "Role": { "Fn::GetAtt": [ "ApiReplicationsPostRole", "Arn" ] }, 2070 | "Runtime": "nodejs10.x", 2071 | "Handler": "index.handler", 2072 | "MemorySize": 128, 2073 | "Timeout": 300, 2074 | "Code": { 2075 | "ZipFile": { 2076 | "Fn::Include": { 2077 | "type": "literal", 2078 | "location": "../lambda/api/tables/replications/post.min.js" 2079 | } 2080 | } 2081 | } 2082 | } 2083 | }, 2084 | "ApiReplicationsPostPermission": { 2085 | "Type": "AWS::Lambda::Permission", 2086 | "Properties": { 2087 | "Action": "lambda:InvokeFunction", 2088 | "FunctionName": { "Ref": "ApiReplicationsPostFunction" }, 2089 | "Principal": "apigateway.amazonaws.com", 2090 | "SourceArn": { "Fn::Join": [ "", [ "arn:aws:execute-api:", { "Ref": "AWS::Region" }, ":", { "Ref": "AWS::AccountId"}, ":", { "Ref" : "ReplicationApi" }, "/*/POST/replications" ] ] } 2091 | } 2092 | }, 2093 | "ApiReplicationsDeleteRole": { 2094 | "Type": "AWS::IAM::Role", 2095 | "Properties": { 2096 | "Path": "/replication/api/replications/delete/", 2097 | "AssumeRolePolicyDocument": { 2098 | "Version": "2012-10-17", 2099 | "Statement": [ 2100 | { 2101 | "Effect": "Allow", 2102 | "Principal": { 2103 | "Service": [ 2104 | "lambda.amazonaws.com" 2105 | ] 2106 | }, 2107 | "Action": [ 2108 | "sts:AssumeRole" 2109 | ] 2110 | } 2111 | ] 2112 | }, 2113 | "Policies": [ 2114 | { 2115 | "PolicyName": "ApiReplicationsDeletePolicy", 2116 | "PolicyDocument": { 2117 | "Version": "2012-10-17", 2118 | "Statement": [ 2119 | { 2120 | "Sid": "LambdaLogging", 2121 | "Action": [ 2122 | "logs:CreateLogGroup", 2123 | "logs:CreateLogStream", 2124 | "logs:PutLogEvents" 2125 | ], 2126 | "Effect": "Allow", 2127 | "Resource": "arn:aws:logs:*:*:*" 2128 | }, 2129 | { 2130 | "Sid": "StopReplication", 2131 | "Action": [ 2132 | "dynamodb:UpdateItem" 2133 | ], 2134 | "Effect": "Allow", 2135 | "Resource": { "Fn::Join": [ "", [ "arn:aws:dynamodb:", { "Ref": "AWS::Region" }, ":", { "Ref": "AWS::AccountId" }, ":table/", { "Ref": "ControllerTableName" } ] ] } 2136 | } 2137 | ] 2138 | } 2139 | } 2140 | ] 2141 | } 2142 | }, 2143 | "ApiReplicationsDeleteFunction": { 2144 | "Type": "AWS::Lambda::Function", 2145 | "Properties": { 2146 | "FunctionName": "Replication-API-Replications-Delete", 2147 | "Description": "Removes an existing replication from the controller table", 2148 | "Role": { "Fn::GetAtt": [ "ApiReplicationsDeleteRole", "Arn" ] }, 2149 | "Runtime": "nodejs10.x", 2150 | "Handler": "index.handler", 2151 | "MemorySize": 128, 2152 | "Timeout": 300, 2153 | "Code": { 2154 | "ZipFile": { 2155 | "Fn::Include": { 2156 | "type": "literal", 2157 | "location": "../lambda/api/tables/replications/delete.min.js" 2158 | } 2159 | } 2160 | } 2161 | } 2162 | }, 2163 | "ApiReplicationsDeletePermission": { 2164 | "Type": "AWS::Lambda::Permission", 2165 | "Properties": { 2166 | "Action": "lambda:InvokeFunction", 2167 | "FunctionName": { "Ref": "ApiReplicationsDeleteFunction" }, 2168 | "Principal": "apigateway.amazonaws.com", 2169 | "SourceArn": { "Fn::Join": [ "", [ "arn:aws:execute-api:", { "Ref": "AWS::Region" }, ":", { "Ref": "AWS::AccountId"}, ":", { "Ref" : "ReplicationApi" }, "/*/DELETE/replications/{replication}" ] ] } 2170 | } 2171 | }, 2172 | "ApiPrefixesPostRole": { 2173 | "Type": "AWS::IAM::Role", 2174 | "Properties": { 2175 | "Path": "/replication/api/prefixes/post/", 2176 | "AssumeRolePolicyDocument": { 2177 | "Version": "2012-10-17", 2178 | "Statement": [ 2179 | { 2180 | "Effect": "Allow", 2181 | "Principal": { 2182 | "Service": [ 2183 | "lambda.amazonaws.com" 2184 | ] 2185 | }, 2186 | "Action": [ 2187 | "sts:AssumeRole" 2188 | ] 2189 | } 2190 | ] 2191 | }, 2192 | "Policies": [ 2193 | { 2194 | "PolicyName": "ApiPrefixesPostPolicy", 2195 | "PolicyDocument": { 2196 | "Version": "2012-10-17", 2197 | "Statement": [ 2198 | { 2199 | "Sid": "LambdaLogging", 2200 | "Effect": "Allow", 2201 | "Action": [ 2202 | "logs:CreateLogGroup", 2203 | "logs:CreateLogStream", 2204 | "logs:PutLogEvents" 2205 | ], 2206 | "Resource": "arn:aws:logs:*:*:*" 2207 | }, 2208 | { 2209 | "Sid": "AddPrefix", 2210 | "Action": [ 2211 | "dynamodb:PutItem" 2212 | ], 2213 | "Effect": "Allow", 2214 | "Resource": { "Fn::Join": [ "", [ "arn:aws:dynamodb:", { "Ref": "AWS::Region" }, ":", { "Ref": "AWS::AccountId" }, ":table/", { "Ref": "PrefixTableName" } ] ] } 2215 | } 2216 | ] 2217 | } 2218 | } 2219 | ] 2220 | } 2221 | }, 2222 | "ApiPrefixesPostFunction": { 2223 | "Type": "AWS::Lambda::Function", 2224 | "Properties": { 2225 | "FunctionName": "Replication-API-Prefixes-Post", 2226 | "Description": "Add a new prefix to the prefix table", 2227 | "Role": { "Fn::GetAtt": [ "ApiPrefixesPostRole", "Arn" ] }, 2228 | "Runtime": "nodejs10.x", 2229 | "Handler": "index.handler", 2230 | "MemorySize": 128, 2231 | "Timeout": 300, 2232 | "Code": { 2233 | "ZipFile": { 2234 | "Fn::Include": { 2235 | "type": "literal", 2236 | "location": "../lambda/api/tables/prefixes/post.min.js" 2237 | } 2238 | } 2239 | } 2240 | } 2241 | }, 2242 | "ApiPrefixesPostPermission": { 2243 | "Type": "AWS::Lambda::Permission", 2244 | "Properties": { 2245 | "Action": "lambda:InvokeFunction", 2246 | "FunctionName": { "Ref": "ApiPrefixesPostFunction" }, 2247 | "Principal": "apigateway.amazonaws.com", 2248 | "SourceArn": { "Fn::Join": [ "", [ "arn:aws:execute-api:", { "Ref": "AWS::Region" }, ":", { "Ref": "AWS::AccountId"}, ":", { "Ref" : "ReplicationApi" }, "/*/POST/prefixes" ] ] } 2249 | } 2250 | }, 2251 | "ApiPrefixesDeleteRole": { 2252 | "Type": "AWS::IAM::Role", 2253 | "Properties": { 2254 | "Path": "/replication/api/prefixes/delete/", 2255 | "AssumeRolePolicyDocument": { 2256 | "Version": "2012-10-17", 2257 | "Statement": [ 2258 | { 2259 | "Effect": "Allow", 2260 | "Principal": { 2261 | "Service": [ 2262 | "lambda.amazonaws.com" 2263 | ] 2264 | }, 2265 | "Action": [ 2266 | "sts:AssumeRole" 2267 | ] 2268 | } 2269 | ] 2270 | }, 2271 | "Policies": [ 2272 | { 2273 | "PolicyName": "ApiPrefixesDeletePolicy", 2274 | "PolicyDocument": { 2275 | "Version": "2012-10-17", 2276 | "Statement": [ 2277 | { 2278 | "Sid": "LambdaLogging", 2279 | "Action": [ 2280 | "logs:CreateLogGroup", 2281 | "logs:CreateLogStream", 2282 | "logs:PutLogEvents" 2283 | ], 2284 | "Effect": "Allow", 2285 | "Resource": "arn:aws:logs:*:*:*" 2286 | }, 2287 | { 2288 | "Sid": "DeletePrefix", 2289 | "Action": [ 2290 | "dynamodb:DeleteItem" 2291 | ], 2292 | "Effect": "Allow", 2293 | "Resource": { "Fn::Join": [ "", [ "arn:aws:dynamodb:", { "Ref": "AWS::Region" }, ":", { "Ref": "AWS::AccountId" }, ":table/", { "Ref": "PrefixTableName" } ] ] } 2294 | } 2295 | ] 2296 | } 2297 | } 2298 | ] 2299 | } 2300 | }, 2301 | "ApiPrefixesDeleteFunction": { 2302 | "Type": "AWS::Lambda::Function", 2303 | "Properties": { 2304 | "FunctionName": "Replication-API-Prefixes-Delete", 2305 | "Description": "Remove an existing prefix from the prefix table", 2306 | "Role": { "Fn::GetAtt": [ "ApiPrefixesDeleteRole", "Arn" ] }, 2307 | "Runtime": "nodejs10.x", 2308 | "Handler": "index.handler", 2309 | "MemorySize": 128, 2310 | "Timeout": 300, 2311 | "Code": { 2312 | "ZipFile": { 2313 | "Fn::Include": { 2314 | "type": "literal", 2315 | "location": "../lambda/api/tables/prefixes/delete.min.js" 2316 | } 2317 | } 2318 | } 2319 | } 2320 | }, 2321 | "ApiPrefixesDeletePermission": { 2322 | "Type": "AWS::Lambda::Permission", 2323 | "Properties": { 2324 | "Action": "lambda:InvokeFunction", 2325 | "FunctionName": { "Ref": "ApiPrefixesDeleteFunction" }, 2326 | "Principal": "apigateway.amazonaws.com", 2327 | "SourceArn": { "Fn::Join": [ "", [ "arn:aws:execute-api:", { "Ref": "AWS::Region" }, ":", { "Ref": "AWS::AccountId"}, ":", { "Ref" : "ReplicationApi" }, "/*/DELETE/prefixes/{prefix}" ] ] } 2328 | } 2329 | }, 2330 | "ApiPrefixesPostRole": { 2331 | "Type": "AWS::IAM::Role", 2332 | "Properties": { 2333 | "Path": "/replication/api/prefixes/post/", 2334 | "AssumeRolePolicyDocument": { 2335 | "Version": "2012-10-17", 2336 | "Statement": [ 2337 | { 2338 | "Effect": "Allow", 2339 | "Principal": { 2340 | "Service": [ 2341 | "lambda.amazonaws.com" 2342 | ] 2343 | }, 2344 | "Action": [ 2345 | "sts:AssumeRole" 2346 | ] 2347 | } 2348 | ] 2349 | }, 2350 | "Policies": [ 2351 | { 2352 | "PolicyName": "ApiPrefixesPostPolicy", 2353 | "PolicyDocument": { 2354 | "Version": "2012-10-17", 2355 | "Statement": [ 2356 | { 2357 | "Sid": "LambdaLogging", 2358 | "Effect": "Allow", 2359 | "Action": [ 2360 | "logs:CreateLogGroup", 2361 | "logs:CreateLogStream", 2362 | "logs:PutLogEvents" 2363 | ], 2364 | "Resource": "arn:aws:logs:*:*:*" 2365 | }, 2366 | { 2367 | "Sid": "AddPrefix", 2368 | "Action": [ 2369 | "dynamodb:PutItem" 2370 | ], 2371 | "Effect": "Allow", 2372 | "Resource": { "Fn::Join": [ "", [ "arn:aws:dynamodb:", { "Ref": "AWS::Region" }, ":", { "Ref": "AWS::AccountId" }, ":table/", { "Ref": "PrefixTableName" } ] ] } 2373 | } 2374 | ] 2375 | } 2376 | } 2377 | ] 2378 | } 2379 | }, 2380 | "ApiPrefixesPostFunction": { 2381 | "Type": "AWS::Lambda::Function", 2382 | "Properties": { 2383 | "FunctionName": "Replication-API-Prefixes-Post", 2384 | "Description": "Add a new prefix to the prefix table", 2385 | "Role": { "Fn::GetAtt": [ "ApiPrefixesPostRole", "Arn" ] }, 2386 | "Runtime": "nodejs10.x", 2387 | "Handler": "index.handler", 2388 | "MemorySize": 128, 2389 | "Timeout": 300, 2390 | "Code": { 2391 | "ZipFile": { 2392 | "Fn::Include": { 2393 | "type": "literal", 2394 | "location": "../lambda/api/tables/prefixes/post.min.js" 2395 | } 2396 | } 2397 | } 2398 | } 2399 | }, 2400 | "ApiPrefixesPostPermission": { 2401 | "Type": "AWS::Lambda::Permission", 2402 | "Properties": { 2403 | "Action": "lambda:InvokeFunction", 2404 | "FunctionName": { "Ref": "ApiPrefixesPostFunction" }, 2405 | "Principal": "apigateway.amazonaws.com", 2406 | "SourceArn": { "Fn::Join": [ "", [ "arn:aws:execute-api:", { "Ref": "AWS::Region" }, ":", { "Ref": "AWS::AccountId"}, ":", { "Ref" : "ReplicationApi" }, "/*/POST/prefixes" ] ] } 2407 | } 2408 | }, 2409 | "ApiMetricsGetRole": { 2410 | "Type": "AWS::IAM::Role", 2411 | "Properties": { 2412 | "Path": "/replication/api/metrics/get/", 2413 | "AssumeRolePolicyDocument": { 2414 | "Version": "2012-10-17", 2415 | "Statement": [ 2416 | { 2417 | "Effect": "Allow", 2418 | "Principal": { 2419 | "Service": [ 2420 | "lambda.amazonaws.com" 2421 | ] 2422 | }, 2423 | "Action": [ 2424 | "sts:AssumeRole" 2425 | ] 2426 | } 2427 | ] 2428 | }, 2429 | "Policies": [ 2430 | { 2431 | "PolicyName": "ApiMetricsGetPolicy", 2432 | "PolicyDocument": { 2433 | "Version": "2012-10-17", 2434 | "Statement": [ 2435 | { 2436 | "Sid": "LambdaLogging", 2437 | "Action": [ 2438 | "logs:CreateLogGroup", 2439 | "logs:CreateLogStream", 2440 | "logs:PutLogEvents" 2441 | ], 2442 | "Effect": "Allow", 2443 | "Resource": "arn:aws:logs:*:*:*" 2444 | }, 2445 | { 2446 | "Sid": "GetMetricStatistics", 2447 | "Action": [ 2448 | "cloudwatch:GetMetricStatistics" 2449 | ], 2450 | "Effect": "Allow", 2451 | "Resource": "*" 2452 | } 2453 | ] 2454 | } 2455 | } 2456 | ] 2457 | } 2458 | }, 2459 | "ApiMetricsGetFunction": { 2460 | "Type": "AWS::Lambda::Function", 2461 | "Properties": { 2462 | "FunctionName": "Replication-API-Metrics-Get", 2463 | "Description": "Get CloudWatch metric statistics", 2464 | "Role": { "Fn::GetAtt": [ "ApiMetricsGetRole", "Arn" ] }, 2465 | "Runtime": "nodejs10.x", 2466 | "Handler": "index.handler", 2467 | "MemorySize": 128, 2468 | "Timeout": 300, 2469 | "Code": { 2470 | "ZipFile": { 2471 | "Fn::Include": { 2472 | "type": "literal", 2473 | "location": "../lambda/api/metrics/get.min.js" 2474 | } 2475 | } 2476 | } 2477 | } 2478 | }, 2479 | "ApiMetricsGetPermission": { 2480 | "Type": "AWS::Lambda::Permission", 2481 | "Properties": { 2482 | "Action": "lambda:InvokeFunction", 2483 | "FunctionName": { "Ref": "ApiMetricsGetFunction" }, 2484 | "Principal": "apigateway.amazonaws.com", 2485 | "SourceArn": { "Fn::Join": [ "", [ "arn:aws:execute-api:", { "Ref": "AWS::Region" }, ":", { "Ref": "AWS::AccountId"}, ":", { "Ref" : "ReplicationApi" }, "/*/GET/metrics/{namespace}/{metric}" ] ] } 2486 | } 2487 | } 2488 | }, 2489 | "Outputs": { 2490 | "InvokeURL": { 2491 | "Description": "Base URL for api", 2492 | "Value": { "Fn::Join": [ "", [ "https://", { "Ref": "ReplicationApi" }, ".execute-api.", { "Ref": "AWS::Region" }, ".amazonaws.com/", { "Ref": "ApiStage" } ] ] } 2493 | }, 2494 | "ApiKey": { 2495 | "Description": "API Key for the web console", 2496 | "Value": { "Ref": "ApiKey" } 2497 | } 2498 | } 2499 | } 2500 | -------------------------------------------------------------------------------- /lambda/api/metrics/get.js: -------------------------------------------------------------------------------- 1 | // Dependencies 2 | var AWS = require('aws-sdk'); 3 | var cloudwatch = new AWS.CloudWatch({apiVersion: "2010-08-01"}); 4 | 5 | const levelLogger = { 6 | log: (...args) => console.log( '[LOG]', ...args), 7 | info: (...args) => console.log( '[INFO]', ...args), 8 | warn: (...args) => console.log( '[WARN]', ...args), 9 | error: (...args) => console.log( '[ERROR]', ...args), 10 | }; 11 | 12 | // Main handler function 13 | exports.handler = function(event, context, callback) { 14 | 15 | var namespace = event.namespace; 16 | var metric = event.metric; 17 | var dimension = event.dimension; 18 | 19 | //Span vs count? 20 | var count = event.count; 21 | var period = event.period; 22 | var span = period * count; 23 | var unit = event.unit; 24 | var statistic = event.statistic; 25 | var endTime = new Date(); 26 | var startTime = new Date(endTime); 27 | startTime.setSeconds(startTime.getSeconds() - span); 28 | 29 | var params = { 30 | StartTime: startTime, 31 | EndTime: endTime, 32 | MetricName: metric, 33 | Namespace: namespace, 34 | Period: period, 35 | Statistics: [ statistic ], 36 | Unit: unit 37 | }; 38 | 39 | if(dimension) { 40 | params.Dimensions = [ 41 | { 42 | Name: 'TableName', 43 | Value: dimension 44 | } 45 | ] 46 | } 47 | 48 | cloudwatch.getMetricStatistics(params, function(err, data){ 49 | if(err){ 50 | levelLogger.error("Unable to get metric statistics"); 51 | levelLogger.error(err.name, "-", err.message); 52 | callback(err); 53 | }else { 54 | callback(null, data); 55 | } 56 | }); 57 | }; -------------------------------------------------------------------------------- /lambda/api/tables/get.js: -------------------------------------------------------------------------------- 1 | // Dependencies 2 | var AWS = require('aws-sdk'); 3 | var dynamodb = new AWS.DynamoDB.DocumentClient({ apiVersion: '2012-08-10'}); 4 | 5 | const levelLogger = { 6 | log: (...args) => console.log( '[LOG]', ...args), 7 | info: (...args) => console.log( '[INFO]', ...args), 8 | warn: (...args) => console.log( '[WARN]', ...args), 9 | error: (...args) => console.log( '[ERROR]', ...args), 10 | } 11 | 12 | const prefixLogger = (prefix) => ({ 13 | log: (...args) => levelLogger.log(`[${prefix}]`, ...args), 14 | info: (...args) => levelLogger.info(`[${prefix}]`, ...args), 15 | warn: (...args) => levelLogger.warn(`[${prefix}]`, ...args), 16 | error: (...args) => levelLogger.error(`[${prefix}]`, ...args), 17 | }); 18 | 19 | // Main handler function 20 | var handler = exports.handler = function(event, context, callback){ 21 | 22 | var table = event.table; 23 | const tableLogger = prefixLogger(table); 24 | 25 | //Grab items from table 26 | dynamodb.scan({ 27 | TableName: event.table, 28 | AttributesToGet: event.attributes 29 | }, function(err, data){ 30 | if(err){ 31 | tableLogger.error("Error scanning table"); 32 | tableLogger.error(err.code, "-", err.message); 33 | return callback(err); 34 | } 35 | //Return items 36 | return callback(null, { items: data.Items }); 37 | }); 38 | }; 39 | -------------------------------------------------------------------------------- /lambda/api/tables/prefixes/delete.js: -------------------------------------------------------------------------------- 1 | // Dependencies 2 | var AWS = require('aws-sdk'); 3 | var dynamodb = new AWS.DynamoDB({apiVersion: "2012-08-10"}); 4 | 5 | const levelLogger = { 6 | log: (...args) => console.log( '[LOG]', ...args), 7 | info: (...args) => console.log( '[INFO]', ...args), 8 | warn: (...args) => console.log( '[WARN]', ...args), 9 | error: (...args) => console.log( '[ERROR]', ...args), 10 | } 11 | 12 | const prefixLogger = (prefix) => ({ 13 | log: (...args) => levelLogger.log(`[${prefix}]`, ...args), 14 | info: (...args) => levelLogger.info(`[${prefix}]`, ...args), 15 | warn: (...args) => levelLogger.warn(`[${prefix}]`, ...args), 16 | error: (...args) => levelLogger.error(`[${prefix}]`, ...args), 17 | }); 18 | 19 | // Main handler function 20 | exports.handler = function(event, context, callback){ 21 | 22 | var prefix = event.prefix; 23 | var prefixTable = event.prefixTable; 24 | 25 | const prefixLogger = prefixLogger(prefix); 26 | 27 | // Delete prefix from table 28 | var params = { 29 | TableName: prefixTable, 30 | Key: { 31 | prefix: { S: prefix } 32 | } 33 | }; 34 | 35 | dynamodb.deleteItem(params, function(err, data){ 36 | if(err){ 37 | prefixLogger.error("Unable to delete item from prefix table"); 38 | prefixLogger.error(err.code, "-", err.data); 39 | } 40 | return callback(err); 41 | }); 42 | }; 43 | -------------------------------------------------------------------------------- /lambda/api/tables/prefixes/post.js: -------------------------------------------------------------------------------- 1 | // Dependencies 2 | var AWS = require('aws-sdk'); 3 | var dynamodb = new AWS.DynamoDB({apiVersion: "2012-08-10"}); 4 | 5 | const levelLogger = { 6 | log: (...args) => console.log( '[LOG]', ...args), 7 | info: (...args) => console.log( '[INFO]', ...args), 8 | warn: (...args) => console.log( '[WARN]', ...args), 9 | error: (...args) => console.log( '[ERROR]', ...args), 10 | } 11 | 12 | const prefixLogger = (prefix) => ({ 13 | log: (...args) => levelLogger.log(`[${prefix}]`, ...args), 14 | info: (...args) => levelLogger.info(`[${prefix}]`, ...args), 15 | warn: (...args) => levelLogger.warn(`[${prefix}]`, ...args), 16 | error: (...args) => levelLogger.error(`[${prefix}]`, ...args), 17 | }); 18 | 19 | // Main handler function 20 | exports.handler = function(event, context, callback){ 21 | 22 | var prefix = event.prefix; 23 | var prefixTable = event.prefixTable; 24 | 25 | const prefixLogger = prefixLogger(prefix); 26 | 27 | //Add prefix to table, if exists 28 | var params = { 29 | TableName: prefixTable, 30 | Item: { 31 | prefix: { S: prefix } 32 | }, 33 | ConditionExpression: 'attribute_not_exists (prefix)' 34 | }; 35 | 36 | dynamodb.putItem(params, function(err, data){ 37 | if(err){ 38 | prefixLogger.error("Unable to write prefix to table"); 39 | if(err.code == "ConditionalCheckFailedException"){ 40 | prefixLogger.error("Prefix already exists in table"); 41 | err.message = new Error("Prefix already exists in table"); 42 | } 43 | prefixLogger.error(err.code, "-", err.message); 44 | } 45 | return callback(err); 46 | }); 47 | }; 48 | -------------------------------------------------------------------------------- /lambda/api/tables/replications/delete.js: -------------------------------------------------------------------------------- 1 | // Dependencies 2 | var AWS = require("aws-sdk"); 3 | var dynamodb = new AWS.DynamoDB({apiVersion: "2012-08-10"}); 4 | 5 | const levelLogger = { 6 | log: (...args) => console.log( '[LOG]', ...args), 7 | info: (...args) => console.log( '[INFO]', ...args), 8 | warn: (...args) => console.log( '[WARN]', ...args), 9 | error: (...args) => console.log( '[ERROR]', ...args), 10 | } 11 | 12 | const prefixLogger = (prefix) => ({ 13 | log: (...args) => levelLogger.log(`[${prefix}]`, ...args), 14 | info: (...args) => levelLogger.info(`[${prefix}]`, ...args), 15 | warn: (...args) => levelLogger.warn(`[${prefix}]`, ...args), 16 | error: (...args) => levelLogger.error(`[${prefix}]`, ...args), 17 | }); 18 | 19 | // Main handler function 20 | exports.handler = function(event, context, callback){ 21 | 22 | var table = event.table; 23 | const tableLogger = prefixLogger(table); 24 | 25 | var params = { 26 | TableName: event.controller, 27 | ConditionExpression: "attribute_exists (tableName)", 28 | Key: { 29 | tableName: { "S" : table } 30 | }, 31 | UpdateExpression: "set #step = :step, #state = :state", 32 | ExpressionAttributeNames: { 33 | "#step": "step", 34 | "#state": "state" 35 | }, 36 | ExpressionAttributeValues: { 37 | ":step": { "S" : "STOP_REPLICATION" }, 38 | ":state": { "S" : "PENDING" } 39 | } 40 | }; 41 | 42 | dynamodb.updateItem(params, function(err, data){ 43 | if(err){ 44 | if(err.code == "ConditionalCheckFailedException"){ 45 | tableLogger.error("Trying to delete a replication doesn't exist"); 46 | err.message = "Replication not found in controller table"; 47 | }else{ 48 | tableLogger.error("Unable to update item"); 49 | } 50 | tableLogger.error(err.code, "-", err.message); 51 | return callback(err); 52 | } 53 | 54 | tableLogger.log("Item updated succesfully"); 55 | callback(err, data); 56 | }); 57 | }; 58 | -------------------------------------------------------------------------------- /lambda/api/tables/replications/post.js: -------------------------------------------------------------------------------- 1 | // Dependencies 2 | var AWS = require("aws-sdk"); 3 | var dynamodb = new AWS.DynamoDB({apiVersion: "2012-08-10"}); 4 | 5 | const levelLogger = { 6 | log: (...args) => console.log( '[LOG]', ...args), 7 | info: (...args) => console.log( '[INFO]', ...args), 8 | warn: (...args) => console.log( '[WARN]', ...args), 9 | error: (...args) => console.log( '[ERROR]', ...args), 10 | } 11 | 12 | const prefixLogger = (prefix) => ({ 13 | log: (...args) => levelLogger.log(`[${prefix}]`, ...args), 14 | info: (...args) => levelLogger.info(`[${prefix}]`, ...args), 15 | warn: (...args) => levelLogger.warn(`[${prefix}]`, ...args), 16 | error: (...args) => levelLogger.error(`[${prefix}]`, ...args), 17 | }); 18 | 19 | // Main handler function 20 | exports.handler = function(event, context, callback){ 21 | 22 | var controller = event.controller; 23 | var table = event.table; 24 | 25 | const tableLogger = prefixLogger(table); 26 | 27 | //Verify table exists in source region 28 | dynamodb.describeTable({ TableName: table }, function(err, data){ 29 | if(err){ 30 | if(err.code == "ResourceNotFoundException") 31 | return callback(new Error("Table " + table +" not found in source region")); 32 | else 33 | return callback(err); 34 | } 35 | 36 | //Table exists, create controller entry for table 37 | var params = { 38 | TableName: controller, 39 | ConditionExpression: "attribute_not_exists (tableName)", 40 | Item: { 41 | tableName: { "S" : table }, 42 | step: { "S" : "VALIDATE_SOURCE" }, 43 | state:{ "S" : "PENDING" } 44 | } 45 | }; 46 | 47 | //Add item to controller table 48 | dynamodb.putItem(params, function(err, data){ 49 | if(err){ 50 | if(err.code == "ConditionalCheckFailedException"){ 51 | tableLogger.error("Item already exists in controller table"); 52 | err.message = "Table " + table + " is already being replicated"; 53 | }else{ 54 | tableLogger.error("Unable to write item to table"); 55 | } 56 | tableLogger.error(err.code, "-", err.message); 57 | return callback(err); 58 | } 59 | tableLogger.log("Response :", JSON.stringify(data)); 60 | return callback(); 61 | }); 62 | }); 63 | }; 64 | -------------------------------------------------------------------------------- /lambda/controller/create-replica/index.js: -------------------------------------------------------------------------------- 1 | // Constants 2 | var REPLICA_REGION = "{{replicaRegion}}"; 3 | 4 | // Dependencies 5 | var AWS = require('aws-sdk'); 6 | var sourcedb = new AWS.DynamoDB({apiVersion: '2012-08-10', region: process.env.AWS_REGION }); 7 | var replicadb = new AWS.DynamoDB({apiVersion: '2012-08-10', region: REPLICA_REGION}); 8 | 9 | const levelLogger = { 10 | log: (...args) => console.log( '[LOG]', ...args), 11 | info: (...args) => console.log( '[INFO]', ...args), 12 | warn: (...args) => console.log( '[WARN]', ...args), 13 | error: (...args) => console.log( '[ERROR]', ...args), 14 | }; 15 | 16 | // Main handler function; 17 | exports.handler = function(event, context, callback){ 18 | 19 | var table = event.table; 20 | 21 | sourcedb.describeTable({ TableName: table }, function(err, data){ 22 | if(err){ 23 | levelLogger.error("Unable to describe table"); 24 | levelLogger.error(err.code, "-", err.message); 25 | return callback(err); 26 | } 27 | 28 | var billingMode = 'PROVISIONED'; 29 | // Check the billing mode of the table 30 | if (data.Table.hasOwnProperty('BillingModeSummary') && 31 | data.Table.BillingModeSummary.BillingMode === 'PAY_PER_REQUEST') { 32 | billingMode = 'PAY_PER_REQUEST'; 33 | } 34 | 35 | //Construct replica table using source table description 36 | var params = { 37 | TableName: data.Table.TableName, 38 | BillingMode: billingMode, 39 | AttributeDefinitions: data.Table.AttributeDefinitions, 40 | KeySchema: data.Table.KeySchema, 41 | StreamSpecification: { 42 | StreamEnabled: true, 43 | StreamViewType: 'NEW_AND_OLD_IMAGES' 44 | } 45 | }; 46 | 47 | // if the billingMode is PROVISIONED, need to add the throughput numbers 48 | if (billingMode === 'PROVISIONED') { 49 | params.ProvisionedThroughput = { 50 | ReadCapacityUnits: data.Table.ProvisionedThroughput.ReadCapacityUnits, 51 | WriteCapacityUnits: data.Table.ProvisionedThroughput.WriteCapacityUnits 52 | }; 53 | } 54 | 55 | if(data.Table.GlobalSecondaryIndexes){ 56 | params.GlobalSecondaryIndexes = processIndexes(billingMode, data.Table.GlobalSecondaryIndexes); 57 | } 58 | if(data.Table.LocalSecondaryIndexes){ 59 | params.LocalSecondaryIndexes = processIndexes(billingMode, data.Table.LocalSecondaryIndexes); 60 | } 61 | 62 | replicadb.createTable(params, function(err, data){ 63 | if(err){ 64 | 65 | if(err.name == "LimitExceededException" || err.name == "InternalServerError"){ 66 | //Retry-able, retry step 67 | levelLogger.warn("Retryable exception encountered -", err.name + ", setting table status to RETRYING"); 68 | return callback(null, { status: "RETRYING", stateMessage: "Retryable exception " + err.name + " encountered" }); 69 | }else if(err.name == 'ResourceInUseException'){ 70 | //Replica table already exists, fail step 71 | levelLogger.error("Table already exists in replica region"); 72 | levelLogger.error(err.name, "-", err.message); 73 | return callback(err); 74 | }else{ 75 | //Non retry-able, fail step 76 | levelLogger.error("Non-retryable exception encountered"); 77 | levelLogger.error(err.name, "-", err.message); 78 | return callback(err); 79 | } 80 | } 81 | callback(); 82 | 83 | }); 84 | }); 85 | 86 | }; 87 | 88 | //Helper function - Construct replica table indexes from source index descriptions 89 | function processIndexes(billingMode, indexes) { 90 | return indexes.reduce(function(newIndexes, index){ 91 | var newIndex = { 92 | IndexName: index.IndexName, 93 | KeySchema: index.KeySchema, 94 | Projection: index.Projection 95 | }; 96 | if (billingMode === 'PROVISIONED') { 97 | if(index.ProvisionedThroughput) { 98 | newIndex.ProvisionedThroughput = { 99 | ReadCapacityUnits: index.ProvisionedThroughput.ReadCapacityUnits, 100 | WriteCapacityUnits: index.ProvisionedThroughput.WriteCapacityUnits 101 | }; 102 | } 103 | } 104 | newIndexes.push(newIndex); 105 | return newIndexes; 106 | }, []); 107 | } 108 | -------------------------------------------------------------------------------- /lambda/controller/index.js: -------------------------------------------------------------------------------- 1 | // Constants 2 | var REPLICATOR = "{{replicatorFunction}}"; 3 | var STEP_MAP = { 4 | "VALIDATE_SOURCE": "{{validateSourceFunction}}", 5 | "VALIDATE_REPLICA": "{{validateReplicaFunction}}", 6 | "CREATE_REPLICA": "{{createReplicaFunction}}", 7 | "START_REPLICATION": "{{startReplicationFunction}}", 8 | "STOP_REPLICATION": "{{stopReplicationFunction}}" 9 | }; 10 | 11 | // Dependencies 12 | var AWS = require('aws-sdk'); 13 | var lambda = new AWS.Lambda({ apiVersion: '2015-03-31' }); 14 | var dynamodb = new AWS.DynamoDB.DocumentClient({ apiVersion: '2012-08-10' }); 15 | 16 | const levelLogger = { 17 | log: (...args) => console.log( '[LOG]', ...args), 18 | info: (...args) => console.log( '[INFO]', ...args), 19 | warn: (...args) => console.log( '[WARN]', ...args), 20 | error: (...args) => console.log( '[ERROR]', ...args), 21 | }; 22 | 23 | // Main handler function 24 | exports.handler = function(event, context, callback){ 25 | 26 | //Function expects batch size of 1 27 | if(!event.Records || !event.Records[0] || event.Records[0].eventSource != 'aws:dynamodb'){ 28 | levelLogger.warn("Invalid event object, no action taken"); 29 | return callback(); 30 | } 31 | 32 | event = event.Records[0]; 33 | 34 | if(event.eventName != 'INSERT' && event.eventName != 'MODIFY'){ 35 | levelLogger.log('No action taken for event', event.eventName, 'on record', JSON.stringify(event)); 36 | return callback(); 37 | } 38 | 39 | var controllerTable; 40 | var tableName; 41 | var state; 42 | var step; 43 | 44 | //Extract data from events 45 | try{ 46 | controllerTable = event.eventSourceARN.split('/')[1]; 47 | tableName = event.dynamodb.NewImage.tableName.S; 48 | state = event.dynamodb.NewImage.state.S; 49 | step = event.dynamodb.NewImage.step.S; 50 | }catch(err){ 51 | levelLogger.warn("Invalid event object, failing step"); 52 | levelLogger.info("Thrown:", err); 53 | return callback(new Error("Internal Error - Lambda function invoked with invalid event object")); 54 | } 55 | 56 | if(state =='COMPLETE') { 57 | //If the state for a step is complete, move to state PENDING on next step 58 | var steps = Object.keys(STEP_MAP); 59 | var nextStepIndex = steps.indexOf(step) + 1; 60 | 61 | if(nextStepIndex < steps.length ){ 62 | var nextStep = { 63 | step: steps[nextStepIndex], 64 | state: 'PENDING' 65 | }; 66 | return updateController(controllerTable, tableName, nextStep, callback); 67 | }else{ 68 | //If all steps are complete, delete item from controller table 69 | return deleteItem(controllerTable, tableName, callback); 70 | } 71 | 72 | }else if(state =='RETRYING'){ 73 | //Set state back to pending to try again 74 | state = 'PENDING'; 75 | } 76 | 77 | if(state != 'PENDING') { 78 | //If not pending, don't take action 79 | levelLogger.log('No action taken for item with state', state); 80 | return callback(); 81 | } 82 | 83 | if(!STEP_MAP[step]) { 84 | //No action for current step, so it fails 85 | levelLogger.warn("No action mapped to step", step); 86 | var items = { 87 | step: step, 88 | state: "FAILED", 89 | stateMessage: "Internal Error - No lambda function mapped to step " + step 90 | }; 91 | return updateController(controllerTable, tableName, items, callback); 92 | } 93 | 94 | var payload = { 95 | controller: controllerTable, 96 | table: tableName, 97 | replicator: REPLICATOR, 98 | record: event.dynamodb.NewImage 99 | }; 100 | 101 | var params = { 102 | FunctionName: STEP_MAP[step], 103 | Payload: JSON.stringify(payload, null, 2) 104 | }; 105 | 106 | 107 | lambda.invoke(params, function(err, response){ 108 | if(err){ 109 | if(err.code == "ResourceNotFoundException"){ 110 | //Step is mapped to a lambda function that doesn't exist, fail 111 | levelLogger.warn("Lambda function", STEP_MAP[step], "(mapped to step " + step + ") does not exist."); 112 | //Simulate a function error, delaying failure until response is processed 113 | response = { 114 | FunctionError: true, 115 | Payload: JSON.stringify({errorMessage: "Internal Error - Lambda function for step " + step + " does not exist"}) 116 | }; 117 | }else{ 118 | //General failure, fail this execution to try again 119 | levelLogger.error("Error invoking lambda function"); 120 | levelLogger.error(err.code, "-", err.message); 121 | return callback(err); 122 | } 123 | } 124 | 125 | var data = JSON.parse(response.Payload); 126 | 127 | var items = {}; 128 | 129 | if(response.FunctionError){ 130 | //Function failed execution, set state to failed, report error message 131 | levelLogger.error("Function error: ", JSON.stringify(data)); 132 | items.step = step; 133 | items.state = 'FAILED'; 134 | items.stateMessage = data.errorMessage; 135 | }else{ 136 | //Set default values for mandatory params 137 | items.step = step; 138 | items.state = 'COMPLETE'; 139 | items.stateMessage= items.step + " " + items.state; 140 | 141 | //Set key value pairs from returned data 142 | for(var key in data){ 143 | if(typeof data[key] != 'string') 144 | data[key] = JSON.stringify(data[key]); 145 | items[key] = data[key]; 146 | } 147 | } 148 | updateController(controllerTable, tableName, items, callback); 149 | }); 150 | }; 151 | 152 | // Helper function to update an item in the controller table 153 | function updateController(controller, table, items, callback){ 154 | var params = { 155 | TableName: controller, 156 | Key: { 157 | tableName: table 158 | }, 159 | UpdateExpression: [], 160 | ExpressionAttributeNames: {}, 161 | ExpressionAttributeValues: {} 162 | }; 163 | 164 | //Construct update using the returned data 165 | for(var key in items){ 166 | params.UpdateExpression.push('#' + key + ' = :' + key); 167 | params.ExpressionAttributeNames['#' + key] = key; 168 | params.ExpressionAttributeValues[':' + key] = items[key]; 169 | } 170 | params.UpdateExpression = 'set ' + params.UpdateExpression.join(', '); 171 | 172 | dynamodb.update(params, function(err, data){ 173 | if(err){ 174 | levelLogger.error("Failed to write to controller table"); 175 | callback(err); 176 | }else{ 177 | callback(); 178 | } 179 | }); 180 | } 181 | 182 | 183 | // Helper function to delete an item in the controller table 184 | function deleteItem(controller, table, callback){ 185 | var params = { 186 | TableName: controller, 187 | Key: { 188 | tableName: table 189 | } 190 | }; 191 | dynamodb.delete(params, function(err, data){ 192 | if(err) 193 | levelLogger.error("Unable to delete item from table"); 194 | return callback(err); 195 | }); 196 | } 197 | -------------------------------------------------------------------------------- /lambda/controller/start-replication/index.js: -------------------------------------------------------------------------------- 1 | // Constants 2 | var STARTING_POSITION = 'TRIM_HORIZON'; 3 | var BATCH_SIZE = 25; 4 | 5 | // Dependencies 6 | var AWS = require('aws-sdk'); 7 | var lambda = new AWS.Lambda({apiVersion: '2015-03-31'}); 8 | 9 | const levelLogger = { 10 | log: (...args) => console.log( '[LOG]', ...args), 11 | info: (...args) => console.log( '[INFO]', ...args), 12 | warn: (...args) => console.log( '[WARN]', ...args), 13 | error: (...args) => console.log( '[ERROR]', ...args), 14 | }; 15 | 16 | // Main handler function 17 | exports.handler = function(event, context, callback) { 18 | 19 | //Verify initialStreamArn exists in event record 20 | if (!event.record.initialStreamArn) { 21 | levelLogger.error("Error - no streamArn provided in event record"); 22 | return callback(new Error("Invalid Event Record - streamArn property missing from event record")); 23 | } 24 | 25 | var params = { 26 | EventSourceArn: event.record.initialStreamArn.S, 27 | FunctionName: event.replicator, 28 | StartingPosition: STARTING_POSITION, 29 | BatchSize: BATCH_SIZE, 30 | Enabled: true 31 | }; 32 | 33 | //Add event source to replicator function 34 | lambda.createEventSourceMapping(params, function (err, data) { 35 | if (err) { 36 | levelLogger.error(err.code, "-", err.message); 37 | levelLogger.error("Unable to create event source mapping for lambda function"); 38 | return callback(err); 39 | } 40 | 41 | //Success, replication is now active 42 | var response = { 43 | state: "ACTIVE", 44 | step: "REPLICATING", 45 | stateMessage: "Succesfully started replication", 46 | UUID: data.UUID 47 | }; 48 | 49 | callback(null, response); 50 | 51 | }); 52 | }; 53 | -------------------------------------------------------------------------------- /lambda/controller/stop-replication/index.js: -------------------------------------------------------------------------------- 1 | // Dependencies 2 | var AWS = require('aws-sdk'); 3 | var lambda = new AWS.Lambda({apiVersion: '2015-03-31'}); 4 | 5 | const levelLogger = { 6 | log: (...args) => console.log( '[LOG]', ...args), 7 | info: (...args) => console.log( '[INFO]', ...args), 8 | warn: (...args) => console.log( '[WARN]', ...args), 9 | error: (...args) => console.log( '[ERROR]', ...args), 10 | }; 11 | 12 | // Main handler function 13 | exports.handler = function(event, context, callback){ 14 | 15 | if(!event.record.UUID){ 16 | levelLogger.warn("No UUID in event record, assuming event source was never created"); 17 | return callback(); 18 | } 19 | 20 | //Grab event source uuid from event 21 | var UUID = event.record.UUID.S; 22 | 23 | //Remove event source mapping 24 | lambda.deleteEventSourceMapping({ UUID : UUID }, function(err, data){ 25 | if(err){ 26 | if(err.code == "ResourceNotFoundException"){ 27 | levelLogger.warn("EventSourceMapping not found, no replication to stop"); 28 | levelLogger.warn("Marking COMPLETE"); 29 | return callback(); 30 | }else{ 31 | levelLogger.error("Failed to delete event source mapping"); 32 | levelLogger.error(err.code, "-", err.message); 33 | return callback(err); 34 | } 35 | } 36 | 37 | callback(); 38 | }); 39 | }; 40 | -------------------------------------------------------------------------------- /lambda/controller/validate-replica/index.js: -------------------------------------------------------------------------------- 1 | // Constants 2 | var REPLICA_REGION = "{{replicaRegion}}"; 3 | 4 | // Dependencies 5 | var AWS = require('aws-sdk'); 6 | var replicadb = new AWS.DynamoDB({apiVersion: '2012-08-10', region: REPLICA_REGION}); 7 | 8 | const levelLogger = { 9 | log: (...args) => console.log( '[LOG]', ...args), 10 | info: (...args) => console.log( '[INFO]', ...args), 11 | warn: (...args) => console.log( '[WARN]', ...args), 12 | error: (...args) => console.log( '[ERROR]', ...args), 13 | }; 14 | 15 | // Main handler function 16 | exports.handler = function(event, context, callback){ 17 | 18 | //Verify keySchema exists in event record 19 | if(!event.record.keySchema){ 20 | levelLogger.error("keySchema property missing from event record"); 21 | return callback(new Error("Invalid Event Record - keySchema property missing from event record")); 22 | } 23 | 24 | var table = event.table; 25 | var keySchema = event.record.keySchema.S; 26 | 27 | replicadb.describeTable({ TableName: table }, function(err, data){ 28 | if(err){ 29 | if (err.code === 'ResourceNotFoundException') { 30 | //Replica Table doesnt exist, create it 31 | levelLogger.log("No replica table found"); 32 | return callback(null, {stateMessage: "No replica table found"}); 33 | }else{ 34 | levelLogger.error("Unable to describe table"); 35 | levelLogger.error(err.code, "-", err.message); 36 | return callback(err); 37 | } 38 | } 39 | 40 | if(JSON.stringify(data.Table.KeySchema) != keySchema){ 41 | //Source and replica key schemas do not match, fail 42 | levelLogger.error("KeySchema on replica does not match that of source"); 43 | return callback(new Error("Source and Replica tables must have the same KeySchema")); 44 | } 45 | 46 | //Valid replica table exists, skip CREATE_REPLICA, go straight to START_REPLICATION 47 | var returnData = { 48 | step: "START_REPLICATION", 49 | state: 'PENDING', 50 | stateMessage: 'Valid replica table found, initializing replication' 51 | }; 52 | 53 | callback(null, returnData); 54 | }); 55 | }; 56 | -------------------------------------------------------------------------------- /lambda/controller/validate-source/index.js: -------------------------------------------------------------------------------- 1 | // Constants 2 | var VIEW_TYPE = "NEW_AND_OLD_IMAGES"; 3 | var GLOBAL_TAG_KEY = "global"; 4 | var GLOBAL_TAG_VALUE = "true"; 5 | 6 | // Dependencies 7 | var AWS = require('aws-sdk'); 8 | var sourcedb = new AWS.DynamoDB({ apiVersion: '2012-08-10', region: process.env.AWS_REGION }); 9 | 10 | const levelLogger = { 11 | log: (...args) => console.log( '[LOG]', ...args), 12 | info: (...args) => console.log( '[INFO]', ...args), 13 | warn: (...args) => console.log( '[WARN]', ...args), 14 | error: (...args) => console.log( '[ERROR]', ...args), 15 | }; 16 | 17 | // Main handler function 18 | exports.handler = function(event, context, callback){ 19 | var tableName = event.table; 20 | 21 | // Wait for the table to be in its final state before trying to list the tags 22 | sourcedb.waitFor('tableExists', {TableName: tableName}, function(err, sourceTable){ 23 | if(err){ 24 | if(err.code == "ResourceNotFoundException"){ 25 | levelLogger.error("Source table not found"); 26 | levelLogger.error(err.code, "-", err.message); 27 | return callback(new Error("Source table could not be found")); 28 | }else{ 29 | levelLogger.error("Unable to describe table"); 30 | levelLogger.error(err.code, "-", err.message); 31 | return callback(err); 32 | } 33 | } 34 | 35 | sourcedb.listTagsOfResource({ ResourceArn: sourceTable.Table.TableArn }, function(err, data){ 36 | if(err){ 37 | levelLogger.error("Unable to list tags for table"); 38 | return callback(err); 39 | } 40 | 41 | 42 | var hasGlobalTag = data.Tags.some(function(tag){ 43 | return tag.Key === GLOBAL_TAG_KEY && tag.Value === GLOBAL_TAG_VALUE; 44 | }); 45 | 46 | if(hasGlobalTag){ 47 | levelLogger.info("Do not replicate source table "+tableName+" because it is a global table"); 48 | return callback(new Error("Do not replicate global tables")); 49 | } 50 | 51 | //Check that stream specification is valid 52 | if(!sourceTable.Table.StreamSpecification || sourceTable.Table.StreamSpecification.StreamEnabled === false || sourceTable.Table.StreamSpecification.StreamViewType != VIEW_TYPE){ 53 | levelLogger.error("Invalid stream specification"); 54 | levelLogger.info("Specification:", JSON.stringify(sourceTable.Table.StreamSpecification) || "None"); 55 | return callback(new Error("Invalid Stream Specification - Streams must be enabled on source table with view type set to " + VIEW_TYPE)); 56 | } 57 | 58 | //Set keySchema and initialStreamArn properties in controller table item 59 | var returnData = { 60 | keySchema: JSON.stringify(sourceTable.Table.KeySchema), 61 | initialStreamArn: sourceTable.Table.LatestStreamArn 62 | }; 63 | return callback(null, returnData); 64 | }); 65 | }); 66 | }; 67 | 68 | //CURRENTLY UNUSED - Helper function - enable stream on source table 69 | function enableStream(tableName, callback){ 70 | var params = { 71 | TableName: tableName, 72 | StreamSpecification: { 73 | StreamEnabled: true, 74 | StreamViewType: VIEW_TYPE 75 | } 76 | }; 77 | sourcedb.updateTable(params, function(err, data){ 78 | if(err){ 79 | callback(err); 80 | }else{ 81 | callback(null, data.Table.LatestStreamArn); 82 | } 83 | }); 84 | } 85 | -------------------------------------------------------------------------------- /lambda/logger.js: -------------------------------------------------------------------------------- 1 | const logMetric = (tableName, context, args) => console.log('[METRIC]', '[' + tableName + ']', context, ...args); 2 | 3 | const metricsLogger = (tableName) => ({ 4 | all: (...args) => logMetric(tableName, 'ALL', args), 5 | table: (...args) => logMetric(tableName, 'TABLE', args), 6 | total: (...args) => logMetric(tableName, 'TOTAL', args), 7 | none: (...args) => logMetric(tableName, 'NONE', args) 8 | }); 9 | 10 | const prefixLogger = (prefix) => ({ 11 | log: (...args) => levelLogger.log(`[${prefix}]`, ...args), 12 | info: (...args) => levelLogger.info(`[${prefix}]`, ...args), 13 | warn: (...args) => levelLogger.warn(`[${prefix}]`, ...args), 14 | error: (...args) => levelLogger.error(`[${prefix}]`, ...args), 15 | }); 16 | 17 | const levelLogger = { 18 | log: (...args) => console.log( '[LOG]', ...args), 19 | info: (...args) => console.log( '[INFO]', ...args), 20 | warn: (...args) => console.log( '[WARN]', ...args), 21 | error: (...args) => console.log( '[ERROR]', ...args), 22 | } 23 | 24 | module.exports = { 25 | metricsLogger, 26 | prefixLogger, 27 | levelLogger 28 | }; 29 | -------------------------------------------------------------------------------- /lambda/replicator/index.js: -------------------------------------------------------------------------------- 1 | // Constants 2 | var REPLICA_REGION = "{{replicaRegion}}"; 3 | var DELAY = [ 100, 200, 400, 800, 1600, 3200 ]; 4 | var MAX_DELAY_INDEX = 5; 5 | var TIMEOUT_PADDING_MS = 2000; 6 | var RETRYABLE = [ "ProvisionedThroughputExceededException", "InternalServerError", "ServiceUnavailable" ]; 7 | 8 | // Dependencies 9 | var AWS = require('aws-sdk'); 10 | var dynamodb = new AWS.DynamoDB({apiVersion: '2012-08-10', region: REPLICA_REGION}); 11 | 12 | const levelLogger = { 13 | log: (...args) => console.log( '[LOG]', ...args), 14 | info: (...args) => console.log( '[INFO]', ...args), 15 | warn: (...args) => console.log( '[WARN]', ...args), 16 | error: (...args) => console.log( '[ERROR]', ...args), 17 | } 18 | 19 | const prefixLogger = (prefix) => ({ 20 | log: (...args) => levelLogger.log(`[${prefix}]`, ...args), 21 | info: (...args) => levelLogger.info(`[${prefix}]`, ...args), 22 | warn: (...args) => levelLogger.warn(`[${prefix}]`, ...args), 23 | error: (...args) => levelLogger.error(`[${prefix}]`, ...args), 24 | }); 25 | 26 | const logMetric = (tableName, context, args) => console.log('[METRIC]', '[' + tableName + ']', context, ...args); 27 | 28 | const metricsLogger = (tableName) => ({ 29 | all: (...args) => logMetric(tableName, 'ALL', args), 30 | table: (...args) => logMetric(tableName, 'TABLE', args), 31 | total: (...args) => logMetric(tableName, 'TOTAL', args), 32 | none: (...args) => logMetric(tableName, 'NONE', args) 33 | }); 34 | 35 | // Handler function 36 | exports.handler = function(event, context, callback){ 37 | //Pull table name from event source arn 38 | var tableName = event.Records[0].eventSourceARN.split('/')[1]; 39 | 40 | const theMetricsLogger = metricsLogger(tableName); 41 | const tableLogger = prefixLogger(tableName); 42 | 43 | //Calculate and post metric for minutes behind record (rounded) 44 | var latestRecordTime= event.Records[event.Records.length - 1].dynamodb.ApproximateCreationDateTime * 1000; 45 | theMetricsLogger.table("MinutesBehindRecord", Math.round((Date.now() - latestRecordTime) / 60000)); 46 | 47 | //For each unique table item, get the latest record 48 | var allRecords = event.Records.reduce(function(allRecords, record) { 49 | var keys= JSON.stringify(record.dynamodb.Keys); 50 | allRecords[keys] = record; 51 | return allRecords; 52 | }, {}); 53 | 54 | //Build request object 55 | var requestItems = {}; 56 | requestItems[tableName] = Object.keys(allRecords).reduce(function(requestItems, key){ 57 | var record = allRecords[key]; 58 | 59 | switch(record.eventName){ 60 | case "INSERT": 61 | case "MODIFY": 62 | requestItems.push({ PutRequest: { Item: record.dynamodb.NewImage } }); 63 | break; 64 | case "REMOVE": 65 | requestItems.push({ DeleteRequest: { Key: record.dynamodb.Keys } }); 66 | break; 67 | default: 68 | tableLogger.warn("Unknown event type '" + record.eventName + "', record will not be processed"); 69 | tableLogger.warn("Record data :", JSON.stringify(record)); 70 | theMetricsLogger.total("UnknownEventTypes"); 71 | break; 72 | } 73 | return requestItems; 74 | }, []); 75 | 76 | (function batchWrite(requestItems, attempt){ 77 | //Write request items to replica table 78 | dynamodb.batchWriteItem({ RequestItems: requestItems }, function(err, data){ 79 | if(err){ 80 | if(RETRYABLE.indexOf(err.code) > -1){ 81 | //Error is retryable, check for / set unprocessed items and warn 82 | tableLogger.warn("Retryable exception encountered :", err.code); 83 | if(data === null || typeof(data) === "undefined"){ 84 | data = {}; 85 | } 86 | if(!data.UnprocessedItems || Object.keys(data.UnprocessedItems).length === 0){ 87 | data.UnprocessedItems = requestItems; 88 | } 89 | }else{ 90 | //Error is not retryable, exit with error 91 | tableLogger.error("Non-retryable exception encountered\n", err.code, "-", err.message); 92 | tableLogger.error("Request Items:", JSON.stringify(requestItems)); 93 | return callback(err); 94 | } 95 | } 96 | 97 | 98 | if(data.UnprocessedItems && Object.keys(data.UnprocessedItems).length !== 0){ 99 | //There is unprocessed items, retry 100 | var delay = DELAY[attempt] || DELAY[MAX_DELAY_INDEX]; 101 | 102 | if(delay + TIMEOUT_PADDING_MS >= context.getRemainingTimeInMillis()){ 103 | //Lambda function will time out before request completes, exit with error 104 | tableLogger.error("Lambda function timed out after", attempt, "attempts"); 105 | tableLogger.error("Request Items:", JSON.stringify(data.UnprocessedItems)); 106 | return callback(new Error("Lambda function timed out after", attempts, "failed attempts")); 107 | } 108 | 109 | //Re-execute this function with unprocessed items after a delay 110 | tableLogger.log("Retrying batch write item request with unprocessed items in " + delay + " seconds"); 111 | setTimeout(batchWrite, delay, data.UnprocessedItems, ++attempt); 112 | }else{ 113 | //There is no unprocessed items, post metrics and exit succesfully 114 | theMetricsLogger.all("RecordsProcessed", event.Records.length); 115 | theMetricsLogger.none("RecordsWritten", Object.keys(allRecords).length); 116 | if(attempt - 1 > 0) 117 | theMetricsLogger.table("ThrottledRequests", attempt - 1); 118 | callback(); 119 | } 120 | }); 121 | })(requestItems, 1); 122 | }; 123 | -------------------------------------------------------------------------------- /lambda/replicator/metrics.js: -------------------------------------------------------------------------------- 1 | //Constants 2 | var NAMESPACE = 'Replication'; 3 | 4 | // Dependencies 5 | var zlib = require('zlib'); 6 | var AWS = require('aws-sdk'); 7 | var cloudwatch = new AWS.CloudWatch({apiVersion: '2010-08-01'}); 8 | 9 | const levelLogger = { 10 | log: (...args) => console.log( '[LOG]', ...args), 11 | info: (...args) => console.log( '[INFO]', ...args), 12 | warn: (...args) => console.log( '[WARN]', ...args), 13 | error: (...args) => console.log( '[ERROR]', ...args), 14 | }; 15 | 16 | // Handler function 17 | exports.handler = function(event, context, callback) { 18 | //Unzip and parse payload 19 | var payload = new Buffer(event.awslogs.data, 'base64'); 20 | zlib.gunzip(payload, function (err, result) { 21 | if (err) { 22 | levelLogger.error("Unable to unzip event"); 23 | return callback(err); 24 | } 25 | 26 | result = JSON.parse(result.toString('utf8')); 27 | 28 | //Process parsed metrics 29 | processMetrics(result, callback); 30 | }); 31 | }; 32 | 33 | function processMetrics(event, callback){ 34 | //Construct putMetricData request object 35 | var metricData = event.logEvents.reduce(function (metricData, logEvent) { 36 | var fields = logEvent.extractedFields; 37 | 38 | if (fields.level === "TOTAL" || fields.level === "ALL") 39 | metricData.push(new MetricDatum(fields.name, fields.value)); 40 | 41 | if (fields.level === "TABLE" || fields.level === "ALL") 42 | metricData.push(new MetricDatum(fields.name, fields.value, fields.table)); 43 | 44 | return metricData; 45 | }, []); 46 | 47 | //Split metrics into groups of 20 48 | var metricGroups = []; 49 | while (metricData.length) { 50 | metricGroups.push(metricData.splice(0, 20)); 51 | } 52 | 53 | //Post metric data for each group 54 | metricGroups.forEach(function (metricGroup) { 55 | var params = { 56 | Namespace: NAMESPACE, 57 | MetricData: metricGroup 58 | }; 59 | cloudwatch.putMetricData(params, function (err, data) { 60 | if (err) { 61 | levelLogger.error("Unable to write metric data to cloudwatch"); 62 | levelLogger.error(err.name, "-", err.message); 63 | levelLogger.error("Failed metric data:", JSON.stringify(metricGroup)); 64 | } 65 | }); 66 | }); 67 | callback(); 68 | } 69 | 70 | // Metric Datum Object Constructor 71 | function MetricDatum(name, value, table){ 72 | this.MetricName = name; 73 | this.Value = value; 74 | this.Unit = "Count"; 75 | 76 | if(table) 77 | this.Dimensions = [ 78 | { 79 | Name: "TableName", 80 | Value: table 81 | } 82 | ] 83 | } 84 | -------------------------------------------------------------------------------- /lambda/watcher/alarm/create.js: -------------------------------------------------------------------------------- 1 | // Constants 2 | var THRESHOLD = "{{delayThreshold}}"; 3 | var PERIOD = 60; 4 | var EVALUATION_PERIODS = 5; 5 | var SNS_TOPIC = "{{snsTopic}}"; 6 | 7 | // Depdendencies 8 | var AWS = require('aws-sdk'); 9 | var cloudwatch = new AWS.CloudWatch({apiVersion: '2010-08-01'}); 10 | 11 | const levelLogger = { 12 | log: (...args) => console.log( '[LOG]', ...args), 13 | info: (...args) => console.log( '[INFO]', ...args), 14 | warn: (...args) => console.log( '[WARN]', ...args), 15 | error: (...args) => console.log( '[ERROR]', ...args), 16 | }; 17 | 18 | // Handler function 19 | exports.handler = function(event, context, callback){ 20 | 21 | // Don't act unless request was successful 22 | if(event.detail.errorCode){ 23 | levelLogger.warn("Request returned an error, event source failed to create"); 24 | levelLogger.warn("No action taken"); 25 | return callback(); 26 | } 27 | 28 | var tableName = event.detail.requestParameters.eventSourceArn.split("/")[1]; 29 | 30 | initMetric(tableName, function(err, data){ 31 | if(err){ 32 | levelLogger.error("Error initializing metric for table", tableName); 33 | levelLogger.error(err.code, "-", err.message); 34 | return callback(err); 35 | } 36 | 37 | createAlarm(tableName, function(err, data){ 38 | if(err){ 39 | levelLogger.error("Error creating alarm for table", tableName); 40 | levelLogger.error(err.code, "-", err.message); 41 | } 42 | 43 | callback(err, data); 44 | }); 45 | }); 46 | }; 47 | 48 | // Pushes a datapoint to initialize the replication's MinutesBehindRecord custom metric 49 | function initMetric(tableName, callback){ 50 | var params = { 51 | Namespace: "Replication", 52 | MetricData: [ 53 | { 54 | MetricName: "MinutesBehindRecord", 55 | Dimensions: [ 56 | { 57 | Name: "TableName", 58 | Value: tableName 59 | } 60 | ], 61 | Timestamp: new Date(), 62 | Unit: "Count", 63 | Value: 0.0 64 | } 65 | ] 66 | }; 67 | 68 | cloudwatch.putMetricData(params, callback); 69 | } 70 | 71 | // Creates an alarm for the new replication's MinutesBehindRecord metric 72 | function createAlarm(tableName, callback){ 73 | var params = { 74 | AlarmName: "Replication-" + tableName + "-MinutesBehindRecord", 75 | AlarmDescription: "Alarms when replication falls too far behind", 76 | Namespace: "Replication", 77 | MetricName: "MinutesBehindRecord", 78 | Dimensions: [ 79 | { 80 | Name: "TableName", 81 | Value: tableName 82 | } 83 | ], 84 | Statistic: "Maximum", 85 | Unit: "Count", 86 | Period: PERIOD, 87 | EvaluationPeriods: EVALUATION_PERIODS, 88 | ComparisonOperator: "GreaterThanOrEqualToThreshold", 89 | Threshold: THRESHOLD, 90 | ActionsEnabled: true, 91 | AlarmActions: [ 92 | SNS_TOPIC 93 | ] 94 | }; 95 | 96 | cloudwatch.putMetricAlarm(params, callback); 97 | } 98 | -------------------------------------------------------------------------------- /lambda/watcher/alarm/delete.js: -------------------------------------------------------------------------------- 1 | // Depdendencies 2 | var AWS = require('aws-sdk'); 3 | var cloudwatch = new AWS.CloudWatch({apiVersion: '2010-08-01'}); 4 | 5 | const levelLogger = { 6 | log: (...args) => console.log( '[LOG]', ...args), 7 | info: (...args) => console.log( '[INFO]', ...args), 8 | warn: (...args) => console.log( '[WARN]', ...args), 9 | error: (...args) => console.log( '[ERROR]', ...args), 10 | }; 11 | 12 | // Handler function 13 | exports.handler = function(event, context, callback){ 14 | 15 | // Don't act unless request was successful 16 | if(event.detail.errorCode){ 17 | levelLogger.warn("Request returned an error, event source failed to delete"); 18 | levelLogger.warn("No action taken"); 19 | return callback(); 20 | } 21 | 22 | var tableName = event.detail.responseElements.eventSourceArn.split("/")[1]; 23 | var alarmName = "Replication-" + tableName + "-MinutesBehindRecord"; 24 | 25 | cloudwatch.deleteAlarms({ AlarmNames: [ alarmName ] }, function(err, data){ 26 | if(err){ 27 | levelLogger.error("Unable to delete alarm"); 28 | levelLogger.error(err.code, "-", err.message); 29 | } 30 | 31 | callback(err); 32 | }); 33 | }; 34 | -------------------------------------------------------------------------------- /lambda/watcher/prefix/create.js: -------------------------------------------------------------------------------- 1 | // Constants 2 | var PREFIX_TABLE = "{{prefixTable}}"; 3 | var CONTROLLER_TABLE = "{{controllerTable}}"; 4 | 5 | // Dependencies 6 | var AWS = require('aws-sdk'); 7 | var dynamodb = new AWS.DynamoDB.DocumentClient({apiVersion: '2012-08-10'}); 8 | 9 | const levelLogger = { 10 | log: (...args) => console.log( '[LOG]', ...args), 11 | info: (...args) => console.log( '[INFO]', ...args), 12 | warn: (...args) => console.log( '[WARN]', ...args), 13 | error: (...args) => console.log( '[ERROR]', ...args), 14 | }; 15 | 16 | // Main handler function 17 | exports.handler = function(event, context, callback){ 18 | //Ensure event was successful 19 | if(event.detail.errorCode){ 20 | levelLogger.warn("Source table failed to create, no action taken"); 21 | return callback(); 22 | } 23 | 24 | //Grab new table name from event object 25 | var table = event.detail.requestParameters.tableName; 26 | 27 | // Retrieve prefix list from dynamodb 28 | dynamodb.scan({ TableName: PREFIX_TABLE }, function(err, data){ 29 | if(err){ 30 | levelLogger.error("Failed to retrieve prefix list from dynamodb"); 31 | levelLogger.error(err.name, "-", err.message); 32 | return callback(err); 33 | } 34 | 35 | //Verify prefixes exist in list 36 | if(!data.Items || data.Items.length === 0){ 37 | levelLogger.log("No prefixes in table"); 38 | return callback(); 39 | } 40 | 41 | //Check if table name uses any of the listed prefixes 42 | if (!data.Items.some(function(pre){ return table.startsWith(pre.prefix);}) ){ 43 | levelLogger.log("No action taken for table", table); 44 | return callback(); 45 | } 46 | 47 | //Table matches prefix, create controller entry for table in initial step/state 48 | var params = { 49 | TableName: CONTROLLER_TABLE, 50 | Item: { 51 | tableName: table, 52 | step:"VALIDATE_SOURCE", 53 | state: "PENDING" 54 | } 55 | }; 56 | 57 | //If this function is being invoked by prefix initialization, don't overwrite existing items 58 | if(event.source == "replication.watcher.prefix.init"){ 59 | params.ConditionExpression = "(attribute_not_exists (tableName)) or (#F = :failed)"; 60 | params.ExpressionAttributeValues = { ":failed": "FAILED" }; 61 | params.ExpressionAttributeNames = { "#F": "state" }; 62 | } 63 | 64 | //Add new entry to controller table 65 | dynamodb.put(params, function(err, data){ 66 | if(err && err.code != "ConditionalCheckFailedException"){ 67 | levelLogger.error("Failed to add table to controller"); 68 | levelLogger.error(err.code, "-", err.message); 69 | return callback(err); 70 | } 71 | callback(); 72 | }); 73 | 74 | }); 75 | }; 76 | -------------------------------------------------------------------------------- /lambda/watcher/prefix/delete.js: -------------------------------------------------------------------------------- 1 | // Constants 2 | var CONTROLLER_TABLE = "{{controllerTable}}"; 3 | 4 | // Dependenceis 5 | var AWS = require('aws-sdk'); 6 | var dynamodb = new AWS.DynamoDB({apiVersion: '2012-08-10'}); 7 | 8 | const levelLogger = { 9 | log: (...args) => console.log( '[LOG]', ...args), 10 | info: (...args) => console.log( '[INFO]', ...args), 11 | warn: (...args) => console.log( '[WARN]', ...args), 12 | error: (...args) => console.log( '[ERROR]', ...args), 13 | }; 14 | 15 | // Main handler function 16 | exports.handler = function(event, context, callback){ 17 | 18 | //Ensure event was successful 19 | if(event.detail.errorCode){ 20 | levelLogger.warn("Table failed to delete, no action taken"); 21 | return callback(); 22 | } 23 | 24 | //Grab table name frome event object 25 | var table = event.detail.requestParameters.tableName; 26 | 27 | //Update the item, if it exists, to the STOP_REPLICATION step 28 | var params = { 29 | TableName: CONTROLLER_TABLE, 30 | ConditionExpression: "attribute_exists (tableName)", 31 | Key: { 32 | tableName: { "S" : table } 33 | }, 34 | UpdateExpression: "set #step = :step, #state = :state", 35 | ExpressionAttributeNames: { 36 | "#step": "step", 37 | "#state": "state" 38 | }, 39 | ExpressionAttributeValues: { 40 | ":step": { "S" : "STOP_REPLICATION" }, 41 | ":state": { "S" : "PENDING" } 42 | } 43 | }; 44 | 45 | dynamodb.updateItem(params, function(err, data){ 46 | if(err){ 47 | if(err.code == "ConditionalCheckFailedException"){ 48 | levelLogger.log("Table", table, "not found in controller, no action taken"); 49 | return callback(); 50 | }else{ 51 | levelLogger.error("Unable to update item"); 52 | levelLogger.error(err.code, "-", err.message); 53 | return callback(err); 54 | } 55 | } 56 | 57 | levelLogger.log("Removal of replication for table", table, "initialized"); 58 | return callback(); 59 | }); 60 | }; 61 | -------------------------------------------------------------------------------- /lambda/watcher/prefix/init.js: -------------------------------------------------------------------------------- 1 | // Dependencies 2 | var AWS = require('aws-sdk'); 3 | var dynamodb = new AWS.DynamoDB({ apiVersion: '2012-08-10' }); 4 | var cloudwatch = new AWS.CloudWatchEvents({ apiVersion: '2015-10-07' }); 5 | 6 | const levelLogger = { 7 | log: (...args) => console.log( '[LOG]', ...args), 8 | info: (...args) => console.log( '[INFO]', ...args), 9 | warn: (...args) => console.log( '[WARN]', ...args), 10 | error: (...args) => console.log( '[ERROR]', ...args), 11 | }; 12 | 13 | // Handler Function 14 | exports.handler = function(event, context, callback){ 15 | 16 | //Only act on new prefixes 17 | if(event.Records[0].eventName !== "INSERT"){ 18 | levelLogger.warn("No action taken for event of type", event.Records[0].eventName); 19 | return callback(); 20 | } 21 | //Grab Prefix from event 22 | var prefix = event.Records[0].dynamodb.NewImage.prefix.S; 23 | 24 | listPrefixedTables(prefix, function(err, tables){ 25 | if(err){ 26 | levelLogger.error("Unable to list tables"); 27 | levelLogger.error(err.code, "-", err.message); 28 | return callback(err); 29 | } 30 | 31 | createReplications(tables, callback); 32 | }); 33 | }; 34 | 35 | // Helper function to create new replications for a list of tables 36 | function createReplications(tables, callback){ 37 | var batches = []; 38 | 39 | // Construct batches of custom cloudwatch event requests 40 | while(tables.length > 0){ 41 | batches.push(tables.splice(0, 10).map(buildEvent)); 42 | } 43 | 44 | // Put events to cloudwatch 45 | (function putEvents(events){ 46 | cloudwatch.putEvents({ Entries: events }, function(err, data){ 47 | if(err){ 48 | levelLogger.error("Unable to post custom CloudWatch events") 49 | levelLogger.error(err.code, "-", err.message); 50 | return callback(err); 51 | } 52 | 53 | // If there are any failed entries, find and retry them 54 | if(data.FailedEntryCount > 0){ 55 | var failedEntries = []; 56 | for(var i = 0; i < data.Entries.length; i++){ 57 | levelLogger.warn(data.FailedEntryCount, " failed entries"); 58 | if(data.Entries[i].ErrorCode){ 59 | levelLogger.warn(data.Entries[i].ErrorCode, "-", data.Entris[i].ErrorMessage); 60 | failedEntries.push(events[i]); 61 | } 62 | } 63 | levelLogger.warn("Retrying put for", data.FailedEntryCount, "entries"); 64 | putEvents(failedEntries); 65 | }else{ 66 | // Loop until no batches are left to be processed 67 | if(batches.length > 0) 68 | putEvents(batches.shift()); 69 | else 70 | return callback(); 71 | } 72 | }); 73 | }(batches.shift())); 74 | } 75 | 76 | // Helper function to find all tables matching the given prefix 77 | function listPrefixedTables(prefix, callback){ 78 | var tables = []; 79 | 80 | (function getPage(lastTable){ 81 | dynamodb.listTables({ ExclusiveStartTableName: lastTable }, function(err, data){ 82 | if(err){ 83 | return callback(err); 84 | } 85 | 86 | tables = tables.concat(data.TableNames); 87 | 88 | if(data.LastEvaluatedTableName){ 89 | //If list is complete, get next set of tables, otherwise 90 | getPage(data.LastEvaluatedTableName); 91 | }else{ 92 | //Return all tables in list starting with a matching prefix 93 | tables = tables.filter(function(table){ return table.startsWith(prefix); }); 94 | callback(null, tables); 95 | } 96 | }); 97 | })(); 98 | } 99 | 100 | // Helper function to build a mock CreateTable event from the table name 101 | function buildEvent(table){ 102 | var detail = { 103 | eventSource: "dynamodb.amazonaws.com", 104 | eventName: "CreateTable", 105 | awsRegion: process.env.AWS_REGION, 106 | requestParameters: { 107 | tableName: table 108 | } 109 | }; 110 | 111 | return { 112 | Detail: JSON.stringify(detail), 113 | DetailType: "Mock API Call via ReplicationWatcher", 114 | Source: "replication.watcher.prefix.init" 115 | }; 116 | } 117 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dynamodb-replication", 3 | "version": "1.0.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "JSV": { 8 | "version": "4.0.2", 9 | "resolved": "https://signiant.jfrog.io/signiant/api/npm/npm-virt/JSV/-/JSV-4.0.2.tgz", 10 | "integrity": "sha1-0Hf2glVx+CEy+d/67Vh7QCn+/1c=", 11 | "dev": true 12 | }, 13 | "abbrev": { 14 | "version": "1.1.1", 15 | "resolved": "https://signiant.jfrog.io/signiant/api/npm/npm-virt/abbrev/-/abbrev-1.1.1.tgz", 16 | "integrity": "sha1-+PLIh60Qv2f2NPAFtph/7TF5qsg=", 17 | "dev": true 18 | }, 19 | "ajv": { 20 | "version": "6.10.2", 21 | "resolved": "https://signiant.jfrog.io/signiant/api/npm/npm-virt/ajv/-/ajv-6.10.2.tgz", 22 | "integrity": "sha1-086gTWsBeyiUrWkED+yLYj60vVI=", 23 | "dev": true, 24 | "requires": { 25 | "fast-deep-equal": "^2.0.1", 26 | "fast-json-stable-stringify": "^2.0.0", 27 | "json-schema-traverse": "^0.4.1", 28 | "uri-js": "^4.2.2" 29 | } 30 | }, 31 | "ansi-styles": { 32 | "version": "1.0.0", 33 | "resolved": "https://signiant.jfrog.io/signiant/api/npm/npm-virt/ansi-styles/-/ansi-styles-1.0.0.tgz", 34 | "integrity": "sha1-yxAt8cVvUSPquLZ817mAJ6AnkXg=", 35 | "dev": true 36 | }, 37 | "argparse": { 38 | "version": "1.0.10", 39 | "resolved": "https://signiant.jfrog.io/signiant/api/npm/npm-virt/argparse/-/argparse-1.0.10.tgz", 40 | "integrity": "sha1-vNZ5HqWuCXJeF+WtmIE0zUCz2RE=", 41 | "dev": true, 42 | "requires": { 43 | "sprintf-js": "~1.0.2" 44 | }, 45 | "dependencies": { 46 | "sprintf-js": { 47 | "version": "1.0.3", 48 | "resolved": "https://signiant.jfrog.io/signiant/api/npm/npm-virt/sprintf-js/-/sprintf-js-1.0.3.tgz", 49 | "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", 50 | "dev": true 51 | } 52 | } 53 | }, 54 | "array-find-index": { 55 | "version": "1.0.2", 56 | "resolved": "https://signiant.jfrog.io/signiant/api/npm/npm-virt/array-find-index/-/array-find-index-1.0.2.tgz", 57 | "integrity": "sha1-3wEKoSh+Fku9pvlyOwqWoexBh6E=", 58 | "dev": true 59 | }, 60 | "asn1": { 61 | "version": "0.2.4", 62 | "resolved": "https://signiant.jfrog.io/signiant/api/npm/npm-virt/asn1/-/asn1-0.2.4.tgz", 63 | "integrity": "sha1-jSR136tVO7M+d7VOWeiAu4ziMTY=", 64 | "dev": true, 65 | "requires": { 66 | "safer-buffer": "~2.1.0" 67 | } 68 | }, 69 | "assert-plus": { 70 | "version": "1.0.0", 71 | "resolved": "https://signiant.jfrog.io/signiant/api/npm/npm-virt/assert-plus/-/assert-plus-1.0.0.tgz", 72 | "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", 73 | "dev": true 74 | }, 75 | "async": { 76 | "version": "1.5.2", 77 | "resolved": "https://signiant.jfrog.io/signiant/api/npm/npm-virt/async/-/async-1.5.2.tgz", 78 | "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=", 79 | "dev": true 80 | }, 81 | "asynckit": { 82 | "version": "0.4.0", 83 | "resolved": "https://signiant.jfrog.io/signiant/api/npm/npm-virt/asynckit/-/asynckit-0.4.0.tgz", 84 | "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", 85 | "dev": true 86 | }, 87 | "aws-sdk": { 88 | "version": "2.559.0", 89 | "resolved": "https://signiant.jfrog.io/signiant/api/npm/npm-virt/aws-sdk/-/aws-sdk-2.559.0.tgz", 90 | "integrity": "sha1-Lzna10KIObT8eof5Q+7xyUoRCuw=", 91 | "dev": true, 92 | "requires": { 93 | "buffer": "4.9.1", 94 | "events": "1.1.1", 95 | "ieee754": "1.1.13", 96 | "jmespath": "0.15.0", 97 | "querystring": "0.2.0", 98 | "sax": "1.2.1", 99 | "url": "0.10.3", 100 | "uuid": "3.3.2", 101 | "xml2js": "0.4.19" 102 | }, 103 | "dependencies": { 104 | "jmespath": { 105 | "version": "0.15.0", 106 | "resolved": "https://signiant.jfrog.io/signiant/api/npm/npm-virt/jmespath/-/jmespath-0.15.0.tgz", 107 | "integrity": "sha1-o/Iiqarp+Wb10nx5ZRDigJF2Qhc=", 108 | "dev": true 109 | } 110 | } 111 | }, 112 | "aws-sign2": { 113 | "version": "0.7.0", 114 | "resolved": "https://signiant.jfrog.io/signiant/api/npm/npm-virt/aws-sign2/-/aws-sign2-0.7.0.tgz", 115 | "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=", 116 | "dev": true 117 | }, 118 | "aws4": { 119 | "version": "1.8.0", 120 | "resolved": "https://signiant.jfrog.io/signiant/api/npm/npm-virt/aws4/-/aws4-1.8.0.tgz", 121 | "integrity": "sha1-8OAD2cqef1nHpQiUXXsu+aBKVC8=", 122 | "dev": true 123 | }, 124 | "balanced-match": { 125 | "version": "1.0.0", 126 | "resolved": "https://signiant.jfrog.io/signiant/api/npm/npm-virt/balanced-match/-/balanced-match-1.0.0.tgz", 127 | "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", 128 | "dev": true 129 | }, 130 | "base64-js": { 131 | "version": "1.3.1", 132 | "resolved": "https://signiant.jfrog.io/signiant/api/npm/npm-virt/base64-js/-/base64-js-1.3.1.tgz", 133 | "integrity": "sha1-WOzoy3XdB+ce0IxzarxfrE2/jfE=", 134 | "dev": true 135 | }, 136 | "bcrypt-pbkdf": { 137 | "version": "1.0.2", 138 | "resolved": "https://signiant.jfrog.io/signiant/api/npm/npm-virt/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", 139 | "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", 140 | "dev": true, 141 | "requires": { 142 | "tweetnacl": "^0.14.3" 143 | } 144 | }, 145 | "bluebird": { 146 | "version": "3.7.1", 147 | "resolved": "https://signiant.jfrog.io/signiant/api/npm/npm-virt/bluebird/-/bluebird-3.7.1.tgz", 148 | "integrity": "sha1-33DjArRx10c0iazyapPWO1P4dN4=", 149 | "dev": true 150 | }, 151 | "brace-expansion": { 152 | "version": "1.1.11", 153 | "resolved": "https://signiant.jfrog.io/signiant/api/npm/npm-virt/brace-expansion/-/brace-expansion-1.1.11.tgz", 154 | "integrity": "sha1-PH/L9SnYcibz0vUrlm/1Jx60Qd0=", 155 | "dev": true, 156 | "requires": { 157 | "balanced-match": "^1.0.0", 158 | "concat-map": "0.0.1" 159 | } 160 | }, 161 | "buffer": { 162 | "version": "4.9.1", 163 | "resolved": "https://signiant.jfrog.io/signiant/api/npm/npm-virt/buffer/-/buffer-4.9.1.tgz", 164 | "integrity": "sha1-bRu2AbB6TvztlwlBMgkwJ8lbwpg=", 165 | "dev": true, 166 | "requires": { 167 | "base64-js": "^1.0.2", 168 | "ieee754": "^1.1.4", 169 | "isarray": "^1.0.0" 170 | } 171 | }, 172 | "buffer-from": { 173 | "version": "1.1.1", 174 | "resolved": "https://signiant.jfrog.io/signiant/api/npm/npm-virt/buffer-from/-/buffer-from-1.1.1.tgz", 175 | "integrity": "sha1-MnE7wCj3XAL9txDXx7zsHyxgcO8=", 176 | "dev": true 177 | }, 178 | "camelcase": { 179 | "version": "2.1.1", 180 | "resolved": "https://signiant.jfrog.io/signiant/api/npm/npm-virt/camelcase/-/camelcase-2.1.1.tgz", 181 | "integrity": "sha1-fB0W1nmhu+WcoCys7PsBHiAfWh8=", 182 | "dev": true 183 | }, 184 | "camelcase-keys": { 185 | "version": "2.1.0", 186 | "resolved": "https://signiant.jfrog.io/signiant/api/npm/npm-virt/camelcase-keys/-/camelcase-keys-2.1.0.tgz", 187 | "integrity": "sha1-MIvur/3ygRkFHvodkyITyRuPkuc=", 188 | "dev": true, 189 | "requires": { 190 | "camelcase": "^2.0.0", 191 | "map-obj": "^1.0.0" 192 | } 193 | }, 194 | "caseless": { 195 | "version": "0.12.0", 196 | "resolved": "https://signiant.jfrog.io/signiant/api/npm/npm-virt/caseless/-/caseless-0.12.0.tgz", 197 | "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=", 198 | "dev": true 199 | }, 200 | "cfn-include": { 201 | "version": "0.6.4", 202 | "resolved": "https://signiant.jfrog.io/signiant/api/npm/npm-virt/cfn-include/-/cfn-include-0.6.4.tgz", 203 | "integrity": "sha1-VTmQ3U8q62Xy3C4IFjzy+uBRNEk=", 204 | "dev": true, 205 | "requires": { 206 | "aws-sdk": "^2.2.17", 207 | "bluebird": "^3.0.5", 208 | "jmespath": "0.14.1", 209 | "jsmin": "1.0.1", 210 | "jsonlint": "^1.6.2", 211 | "lodash": "^4", 212 | "nomnom": "^1.8.1", 213 | "path-parse": "^1.0.5", 214 | "request": "^2.65.0" 215 | } 216 | }, 217 | "chalk": { 218 | "version": "0.4.0", 219 | "resolved": "https://signiant.jfrog.io/signiant/api/npm/npm-virt/chalk/-/chalk-0.4.0.tgz", 220 | "integrity": "sha1-UZmj3c0MHv4jvAjBsCewYXbgxk8=", 221 | "dev": true, 222 | "requires": { 223 | "ansi-styles": "~1.0.0", 224 | "has-color": "~0.1.0", 225 | "strip-ansi": "~0.1.0" 226 | } 227 | }, 228 | "coffeescript": { 229 | "version": "1.10.0", 230 | "resolved": "https://signiant.jfrog.io/signiant/api/npm/npm-virt/coffeescript/-/coffeescript-1.10.0.tgz", 231 | "integrity": "sha1-56qDAZF+9iGzXYo580jc3R234z4=", 232 | "dev": true 233 | }, 234 | "color-convert": { 235 | "version": "1.9.3", 236 | "resolved": "https://signiant.jfrog.io/signiant/api/npm/npm-virt/color-convert/-/color-convert-1.9.3.tgz", 237 | "integrity": "sha1-u3GFBpDh8TZWfeYp0tVHHe2kweg=", 238 | "dev": true, 239 | "requires": { 240 | "color-name": "1.1.3" 241 | } 242 | }, 243 | "color-name": { 244 | "version": "1.1.3", 245 | "resolved": "https://signiant.jfrog.io/signiant/api/npm/npm-virt/color-name/-/color-name-1.1.3.tgz", 246 | "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", 247 | "dev": true 248 | }, 249 | "colors": { 250 | "version": "1.1.2", 251 | "resolved": "https://signiant.jfrog.io/signiant/api/npm/npm-virt/colors/-/colors-1.1.2.tgz", 252 | "integrity": "sha1-FopHAXVran9RoSzgyXv6KMCE7WM=", 253 | "dev": true 254 | }, 255 | "combined-stream": { 256 | "version": "1.0.8", 257 | "resolved": "https://signiant.jfrog.io/signiant/api/npm/npm-virt/combined-stream/-/combined-stream-1.0.8.tgz", 258 | "integrity": "sha1-w9RaizT9cwYxoRCoolIGgrMdWn8=", 259 | "dev": true, 260 | "requires": { 261 | "delayed-stream": "~1.0.0" 262 | } 263 | }, 264 | "commander": { 265 | "version": "2.20.3", 266 | "resolved": "https://signiant.jfrog.io/signiant/api/npm/npm-virt/commander/-/commander-2.20.3.tgz", 267 | "integrity": "sha1-/UhehMA+tIgcIHIrpIA16FMa6zM=", 268 | "dev": true 269 | }, 270 | "concat-map": { 271 | "version": "0.0.1", 272 | "resolved": "https://signiant.jfrog.io/signiant/api/npm/npm-virt/concat-map/-/concat-map-0.0.1.tgz", 273 | "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", 274 | "dev": true 275 | }, 276 | "core-util-is": { 277 | "version": "1.0.2", 278 | "resolved": "https://signiant.jfrog.io/signiant/api/npm/npm-virt/core-util-is/-/core-util-is-1.0.2.tgz", 279 | "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", 280 | "dev": true 281 | }, 282 | "currently-unhandled": { 283 | "version": "0.4.1", 284 | "resolved": "https://signiant.jfrog.io/signiant/api/npm/npm-virt/currently-unhandled/-/currently-unhandled-0.4.1.tgz", 285 | "integrity": "sha1-mI3zP+qxke95mmE2nddsF635V+o=", 286 | "dev": true, 287 | "requires": { 288 | "array-find-index": "^1.0.1" 289 | } 290 | }, 291 | "dashdash": { 292 | "version": "1.14.1", 293 | "resolved": "https://signiant.jfrog.io/signiant/api/npm/npm-virt/dashdash/-/dashdash-1.14.1.tgz", 294 | "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", 295 | "dev": true, 296 | "requires": { 297 | "assert-plus": "^1.0.0" 298 | } 299 | }, 300 | "dateformat": { 301 | "version": "1.0.12", 302 | "resolved": "https://signiant.jfrog.io/signiant/api/npm/npm-virt/dateformat/-/dateformat-1.0.12.tgz", 303 | "integrity": "sha1-nxJLZ1lMk3/3BpMuSmQsyo27/uk=", 304 | "dev": true, 305 | "requires": { 306 | "get-stdin": "^4.0.1", 307 | "meow": "^3.3.0" 308 | } 309 | }, 310 | "decamelize": { 311 | "version": "1.2.0", 312 | "resolved": "https://signiant.jfrog.io/signiant/api/npm/npm-virt/decamelize/-/decamelize-1.2.0.tgz", 313 | "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", 314 | "dev": true 315 | }, 316 | "delayed-stream": { 317 | "version": "1.0.0", 318 | "resolved": "https://signiant.jfrog.io/signiant/api/npm/npm-virt/delayed-stream/-/delayed-stream-1.0.0.tgz", 319 | "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", 320 | "dev": true 321 | }, 322 | "ecc-jsbn": { 323 | "version": "0.1.2", 324 | "resolved": "https://signiant.jfrog.io/signiant/api/npm/npm-virt/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", 325 | "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", 326 | "dev": true, 327 | "requires": { 328 | "jsbn": "~0.1.0", 329 | "safer-buffer": "^2.1.0" 330 | } 331 | }, 332 | "error-ex": { 333 | "version": "1.3.2", 334 | "resolved": "https://signiant.jfrog.io/signiant/api/npm/npm-virt/error-ex/-/error-ex-1.3.2.tgz", 335 | "integrity": "sha1-tKxAZIEH/c3PriQvQovqihTU8b8=", 336 | "dev": true, 337 | "requires": { 338 | "is-arrayish": "^0.2.1" 339 | } 340 | }, 341 | "escape-string-regexp": { 342 | "version": "1.0.5", 343 | "resolved": "https://signiant.jfrog.io/signiant/api/npm/npm-virt/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", 344 | "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", 345 | "dev": true 346 | }, 347 | "esprima": { 348 | "version": "4.0.1", 349 | "resolved": "https://signiant.jfrog.io/signiant/api/npm/npm-virt/esprima/-/esprima-4.0.1.tgz", 350 | "integrity": "sha1-E7BM2z5sXRnfkatph6hpVhmwqnE=", 351 | "dev": true 352 | }, 353 | "eventemitter2": { 354 | "version": "0.4.14", 355 | "resolved": "https://signiant.jfrog.io/signiant/api/npm/npm-virt/eventemitter2/-/eventemitter2-0.4.14.tgz", 356 | "integrity": "sha1-j2G3XN4BKy6esoTUVFWDtWQ7Yas=", 357 | "dev": true 358 | }, 359 | "events": { 360 | "version": "1.1.1", 361 | "resolved": "https://signiant.jfrog.io/signiant/api/npm/npm-virt/events/-/events-1.1.1.tgz", 362 | "integrity": "sha1-nr23Y1rQmccNzEwqH1AEKI6L2SQ=", 363 | "dev": true 364 | }, 365 | "exit": { 366 | "version": "0.1.2", 367 | "resolved": "https://signiant.jfrog.io/signiant/api/npm/npm-virt/exit/-/exit-0.1.2.tgz", 368 | "integrity": "sha1-BjJjj42HfMghB9MKD/8aF8uhzQw=", 369 | "dev": true 370 | }, 371 | "extend": { 372 | "version": "3.0.2", 373 | "resolved": "https://signiant.jfrog.io/signiant/api/npm/npm-virt/extend/-/extend-3.0.2.tgz", 374 | "integrity": "sha1-+LETa0Bx+9jrFAr/hYsQGewpFfo=", 375 | "dev": true 376 | }, 377 | "extsprintf": { 378 | "version": "1.3.0", 379 | "resolved": "https://signiant.jfrog.io/signiant/api/npm/npm-virt/extsprintf/-/extsprintf-1.3.0.tgz", 380 | "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=", 381 | "dev": true 382 | }, 383 | "fast-deep-equal": { 384 | "version": "2.0.1", 385 | "resolved": "https://signiant.jfrog.io/signiant/api/npm/npm-virt/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", 386 | "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=", 387 | "dev": true 388 | }, 389 | "fast-json-stable-stringify": { 390 | "version": "2.0.0", 391 | "resolved": "https://signiant.jfrog.io/signiant/api/npm/npm-virt/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", 392 | "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=", 393 | "dev": true 394 | }, 395 | "find-up": { 396 | "version": "1.1.2", 397 | "resolved": "https://signiant.jfrog.io/signiant/api/npm/npm-virt/find-up/-/find-up-1.1.2.tgz", 398 | "integrity": "sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8=", 399 | "dev": true, 400 | "requires": { 401 | "path-exists": "^2.0.0", 402 | "pinkie-promise": "^2.0.0" 403 | } 404 | }, 405 | "findup-sync": { 406 | "version": "0.3.0", 407 | "resolved": "https://signiant.jfrog.io/signiant/api/npm/npm-virt/findup-sync/-/findup-sync-0.3.0.tgz", 408 | "integrity": "sha1-N5MKpdgWt3fANEXhlmzGeQpMCxY=", 409 | "dev": true, 410 | "requires": { 411 | "glob": "~5.0.0" 412 | }, 413 | "dependencies": { 414 | "glob": { 415 | "version": "5.0.15", 416 | "resolved": "https://signiant.jfrog.io/signiant/api/npm/npm-virt/glob/-/glob-5.0.15.tgz", 417 | "integrity": "sha1-G8k2ueAvSmA/zCIuz3Yz0wuLk7E=", 418 | "dev": true, 419 | "requires": { 420 | "inflight": "^1.0.4", 421 | "inherits": "2", 422 | "minimatch": "2 || 3", 423 | "once": "^1.3.0", 424 | "path-is-absolute": "^1.0.0" 425 | } 426 | } 427 | } 428 | }, 429 | "forever-agent": { 430 | "version": "0.6.1", 431 | "resolved": "https://signiant.jfrog.io/signiant/api/npm/npm-virt/forever-agent/-/forever-agent-0.6.1.tgz", 432 | "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=", 433 | "dev": true 434 | }, 435 | "form-data": { 436 | "version": "2.3.3", 437 | "resolved": "https://signiant.jfrog.io/signiant/api/npm/npm-virt/form-data/-/form-data-2.3.3.tgz", 438 | "integrity": "sha1-3M5SwF9kTymManq5Nr1yTO/786Y=", 439 | "dev": true, 440 | "requires": { 441 | "asynckit": "^0.4.0", 442 | "combined-stream": "^1.0.6", 443 | "mime-types": "^2.1.12" 444 | } 445 | }, 446 | "fs.realpath": { 447 | "version": "1.0.0", 448 | "resolved": "https://signiant.jfrog.io/signiant/api/npm/npm-virt/fs.realpath/-/fs.realpath-1.0.0.tgz", 449 | "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", 450 | "dev": true 451 | }, 452 | "get-stdin": { 453 | "version": "4.0.1", 454 | "resolved": "https://signiant.jfrog.io/signiant/api/npm/npm-virt/get-stdin/-/get-stdin-4.0.1.tgz", 455 | "integrity": "sha1-uWjGsKBDhDJJAui/Gl3zJXmkUP4=", 456 | "dev": true 457 | }, 458 | "getobject": { 459 | "version": "0.1.0", 460 | "resolved": "https://signiant.jfrog.io/signiant/api/npm/npm-virt/getobject/-/getobject-0.1.0.tgz", 461 | "integrity": "sha1-BHpEl4n6Fg0Bj1SG7ZEyC27HiFw=", 462 | "dev": true 463 | }, 464 | "getpass": { 465 | "version": "0.1.7", 466 | "resolved": "https://signiant.jfrog.io/signiant/api/npm/npm-virt/getpass/-/getpass-0.1.7.tgz", 467 | "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", 468 | "dev": true, 469 | "requires": { 470 | "assert-plus": "^1.0.0" 471 | } 472 | }, 473 | "glob": { 474 | "version": "7.0.6", 475 | "resolved": "https://signiant.jfrog.io/signiant/api/npm/npm-virt/glob/-/glob-7.0.6.tgz", 476 | "integrity": "sha1-IRuvr0nlJbjNkyYNFKsTYVKz9Xo=", 477 | "dev": true, 478 | "requires": { 479 | "fs.realpath": "^1.0.0", 480 | "inflight": "^1.0.4", 481 | "inherits": "2", 482 | "minimatch": "^3.0.2", 483 | "once": "^1.3.0", 484 | "path-is-absolute": "^1.0.0" 485 | } 486 | }, 487 | "graceful-fs": { 488 | "version": "4.2.3", 489 | "resolved": "https://signiant.jfrog.io/signiant/api/npm/npm-virt/graceful-fs/-/graceful-fs-4.2.3.tgz", 490 | "integrity": "sha1-ShL/G2A3bvCYYsIJPt2Qgyi+hCM=", 491 | "dev": true 492 | }, 493 | "grunt": { 494 | "version": "1.0.4", 495 | "resolved": "https://signiant.jfrog.io/signiant/api/npm/npm-virt/grunt/-/grunt-1.0.4.tgz", 496 | "integrity": "sha1-x5mIOUWlOj0HYi4HN8j3C/4Z6zg=", 497 | "dev": true, 498 | "requires": { 499 | "coffeescript": "~1.10.0", 500 | "dateformat": "~1.0.12", 501 | "eventemitter2": "~0.4.13", 502 | "exit": "~0.1.1", 503 | "findup-sync": "~0.3.0", 504 | "glob": "~7.0.0", 505 | "grunt-cli": "~1.2.0", 506 | "grunt-known-options": "~1.1.0", 507 | "grunt-legacy-log": "~2.0.0", 508 | "grunt-legacy-util": "~1.1.1", 509 | "iconv-lite": "~0.4.13", 510 | "js-yaml": "~3.13.0", 511 | "minimatch": "~3.0.2", 512 | "mkdirp": "~0.5.1", 513 | "nopt": "~3.0.6", 514 | "path-is-absolute": "~1.0.0", 515 | "rimraf": "~2.6.2" 516 | }, 517 | "dependencies": { 518 | "grunt-cli": { 519 | "version": "1.2.0", 520 | "resolved": "https://signiant.jfrog.io/signiant/api/npm/npm-virt/grunt-cli/-/grunt-cli-1.2.0.tgz", 521 | "integrity": "sha1-VisRnrsGndtGSs4oRVAb6Xs1tqg=", 522 | "dev": true, 523 | "requires": { 524 | "findup-sync": "~0.3.0", 525 | "grunt-known-options": "~1.1.0", 526 | "nopt": "~3.0.6", 527 | "resolve": "~1.1.0" 528 | } 529 | }, 530 | "resolve": { 531 | "version": "1.1.7", 532 | "resolved": "https://signiant.jfrog.io/signiant/api/npm/npm-virt/resolve/-/resolve-1.1.7.tgz", 533 | "integrity": "sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs=", 534 | "dev": true 535 | } 536 | } 537 | }, 538 | "grunt-contrib-clean": { 539 | "version": "1.1.0", 540 | "resolved": "https://signiant.jfrog.io/signiant/api/npm/npm-virt/grunt-contrib-clean/-/grunt-contrib-clean-1.1.0.tgz", 541 | "integrity": "sha1-Vkq/LQN4qYOhW54/MO51tzjEBjg=", 542 | "dev": true, 543 | "requires": { 544 | "async": "^1.5.2", 545 | "rimraf": "^2.5.1" 546 | } 547 | }, 548 | "grunt-known-options": { 549 | "version": "1.1.1", 550 | "resolved": "https://signiant.jfrog.io/signiant/api/npm/npm-virt/grunt-known-options/-/grunt-known-options-1.1.1.tgz", 551 | "integrity": "sha1-bMCIEHvQIZ3F0+V9kZI/RpBZgE0=", 552 | "dev": true 553 | }, 554 | "grunt-legacy-log": { 555 | "version": "2.0.0", 556 | "resolved": "https://signiant.jfrog.io/signiant/api/npm/npm-virt/grunt-legacy-log/-/grunt-legacy-log-2.0.0.tgz", 557 | "integrity": "sha1-yM0sbIGkRlubvy2HTZY/73pZ/7k=", 558 | "dev": true, 559 | "requires": { 560 | "colors": "~1.1.2", 561 | "grunt-legacy-log-utils": "~2.0.0", 562 | "hooker": "~0.2.3", 563 | "lodash": "~4.17.5" 564 | } 565 | }, 566 | "grunt-legacy-log-utils": { 567 | "version": "2.0.1", 568 | "resolved": "https://signiant.jfrog.io/signiant/api/npm/npm-virt/grunt-legacy-log-utils/-/grunt-legacy-log-utils-2.0.1.tgz", 569 | "integrity": "sha1-0vRCx8AVAGXZAEsI/XQQ03UZGU4=", 570 | "dev": true, 571 | "requires": { 572 | "chalk": "~2.4.1", 573 | "lodash": "~4.17.10" 574 | }, 575 | "dependencies": { 576 | "ansi-styles": { 577 | "version": "3.2.1", 578 | "resolved": "https://signiant.jfrog.io/signiant/api/npm/npm-virt/ansi-styles/-/ansi-styles-3.2.1.tgz", 579 | "integrity": "sha1-QfuyAkPlCxK+DwS43tvwdSDOhB0=", 580 | "dev": true, 581 | "requires": { 582 | "color-convert": "^1.9.0" 583 | } 584 | }, 585 | "chalk": { 586 | "version": "2.4.2", 587 | "resolved": "https://signiant.jfrog.io/signiant/api/npm/npm-virt/chalk/-/chalk-2.4.2.tgz", 588 | "integrity": "sha1-zUJUFnelQzPPVBpJEIwUMrRMlCQ=", 589 | "dev": true, 590 | "requires": { 591 | "ansi-styles": "^3.2.1", 592 | "escape-string-regexp": "^1.0.5", 593 | "supports-color": "^5.3.0" 594 | } 595 | } 596 | } 597 | }, 598 | "grunt-legacy-util": { 599 | "version": "1.1.1", 600 | "resolved": "https://signiant.jfrog.io/signiant/api/npm/npm-virt/grunt-legacy-util/-/grunt-legacy-util-1.1.1.tgz", 601 | "integrity": "sha1-4QYk58hgNOW4cMioYWdD8KCEXkI=", 602 | "dev": true, 603 | "requires": { 604 | "async": "~1.5.2", 605 | "exit": "~0.1.1", 606 | "getobject": "~0.1.0", 607 | "hooker": "~0.2.3", 608 | "lodash": "~4.17.10", 609 | "underscore.string": "~3.3.4", 610 | "which": "~1.3.0" 611 | } 612 | }, 613 | "grunt-terser": { 614 | "version": "1.0.0", 615 | "resolved": "https://signiant.jfrog.io/signiant/api/npm/npm-virt/grunt-terser/-/grunt-terser-1.0.0.tgz", 616 | "integrity": "sha1-y0bWRf5R7Js7yx8329LzES2R55E=", 617 | "dev": true, 618 | "requires": { 619 | "terser": "^4.3.9" 620 | } 621 | }, 622 | "har-schema": { 623 | "version": "2.0.0", 624 | "resolved": "https://signiant.jfrog.io/signiant/api/npm/npm-virt/har-schema/-/har-schema-2.0.0.tgz", 625 | "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=", 626 | "dev": true 627 | }, 628 | "har-validator": { 629 | "version": "5.1.3", 630 | "resolved": "https://signiant.jfrog.io/signiant/api/npm/npm-virt/har-validator/-/har-validator-5.1.3.tgz", 631 | "integrity": "sha1-HvievT5JllV2de7ZiTEQ3DUPoIA=", 632 | "dev": true, 633 | "requires": { 634 | "ajv": "^6.5.5", 635 | "har-schema": "^2.0.0" 636 | } 637 | }, 638 | "has-color": { 639 | "version": "0.1.7", 640 | "resolved": "https://signiant.jfrog.io/signiant/api/npm/npm-virt/has-color/-/has-color-0.1.7.tgz", 641 | "integrity": "sha1-ZxRKUmDDT8PMpnfQQdr1L+e3iy8=", 642 | "dev": true 643 | }, 644 | "has-flag": { 645 | "version": "3.0.0", 646 | "resolved": "https://signiant.jfrog.io/signiant/api/npm/npm-virt/has-flag/-/has-flag-3.0.0.tgz", 647 | "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", 648 | "dev": true 649 | }, 650 | "hooker": { 651 | "version": "0.2.3", 652 | "resolved": "https://signiant.jfrog.io/signiant/api/npm/npm-virt/hooker/-/hooker-0.2.3.tgz", 653 | "integrity": "sha1-uDT3I8xKJCqmWWNFnfbZhMXT2Vk=", 654 | "dev": true 655 | }, 656 | "hosted-git-info": { 657 | "version": "2.8.5", 658 | "resolved": "https://signiant.jfrog.io/signiant/api/npm/npm-virt/hosted-git-info/-/hosted-git-info-2.8.5.tgz", 659 | "integrity": "sha1-dZz88sTRVq3lmwst+r3cQqa5xww=", 660 | "dev": true 661 | }, 662 | "http-signature": { 663 | "version": "1.2.0", 664 | "resolved": "https://signiant.jfrog.io/signiant/api/npm/npm-virt/http-signature/-/http-signature-1.2.0.tgz", 665 | "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", 666 | "dev": true, 667 | "requires": { 668 | "assert-plus": "^1.0.0", 669 | "jsprim": "^1.2.2", 670 | "sshpk": "^1.7.0" 671 | } 672 | }, 673 | "iconv-lite": { 674 | "version": "0.4.24", 675 | "resolved": "https://signiant.jfrog.io/signiant/api/npm/npm-virt/iconv-lite/-/iconv-lite-0.4.24.tgz", 676 | "integrity": "sha1-ICK0sl+93CHS9SSXSkdKr+czkIs=", 677 | "dev": true, 678 | "requires": { 679 | "safer-buffer": ">= 2.1.2 < 3" 680 | } 681 | }, 682 | "ieee754": { 683 | "version": "1.1.13", 684 | "resolved": "https://signiant.jfrog.io/signiant/api/npm/npm-virt/ieee754/-/ieee754-1.1.13.tgz", 685 | "integrity": "sha1-7BaFWOlaoYH9h9N/VcMrvLZwi4Q=", 686 | "dev": true 687 | }, 688 | "indent-string": { 689 | "version": "2.1.0", 690 | "resolved": "https://signiant.jfrog.io/signiant/api/npm/npm-virt/indent-string/-/indent-string-2.1.0.tgz", 691 | "integrity": "sha1-ji1INIdCEhtKghi3oTfppSBJ3IA=", 692 | "dev": true, 693 | "requires": { 694 | "repeating": "^2.0.0" 695 | } 696 | }, 697 | "inflight": { 698 | "version": "1.0.6", 699 | "resolved": "https://signiant.jfrog.io/signiant/api/npm/npm-virt/inflight/-/inflight-1.0.6.tgz", 700 | "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", 701 | "dev": true, 702 | "requires": { 703 | "once": "^1.3.0", 704 | "wrappy": "1" 705 | } 706 | }, 707 | "inherits": { 708 | "version": "2.0.4", 709 | "resolved": "https://signiant.jfrog.io/signiant/api/npm/npm-virt/inherits/-/inherits-2.0.4.tgz", 710 | "integrity": "sha1-D6LGT5MpF8NDOg3tVTY6rjdBa3w=", 711 | "dev": true 712 | }, 713 | "is-arrayish": { 714 | "version": "0.2.1", 715 | "resolved": "https://signiant.jfrog.io/signiant/api/npm/npm-virt/is-arrayish/-/is-arrayish-0.2.1.tgz", 716 | "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=", 717 | "dev": true 718 | }, 719 | "is-finite": { 720 | "version": "1.0.2", 721 | "resolved": "https://signiant.jfrog.io/signiant/api/npm/npm-virt/is-finite/-/is-finite-1.0.2.tgz", 722 | "integrity": "sha1-zGZ3aVYCvlUO8R6LSqYwU0K20Ko=", 723 | "dev": true, 724 | "requires": { 725 | "number-is-nan": "^1.0.0" 726 | } 727 | }, 728 | "is-typedarray": { 729 | "version": "1.0.0", 730 | "resolved": "https://signiant.jfrog.io/signiant/api/npm/npm-virt/is-typedarray/-/is-typedarray-1.0.0.tgz", 731 | "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=", 732 | "dev": true 733 | }, 734 | "is-utf8": { 735 | "version": "0.2.1", 736 | "resolved": "https://signiant.jfrog.io/signiant/api/npm/npm-virt/is-utf8/-/is-utf8-0.2.1.tgz", 737 | "integrity": "sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI=", 738 | "dev": true 739 | }, 740 | "isarray": { 741 | "version": "1.0.0", 742 | "resolved": "https://signiant.jfrog.io/signiant/api/npm/npm-virt/isarray/-/isarray-1.0.0.tgz", 743 | "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", 744 | "dev": true 745 | }, 746 | "isexe": { 747 | "version": "2.0.0", 748 | "resolved": "https://signiant.jfrog.io/signiant/api/npm/npm-virt/isexe/-/isexe-2.0.0.tgz", 749 | "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", 750 | "dev": true 751 | }, 752 | "isstream": { 753 | "version": "0.1.2", 754 | "resolved": "https://signiant.jfrog.io/signiant/api/npm/npm-virt/isstream/-/isstream-0.1.2.tgz", 755 | "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=", 756 | "dev": true 757 | }, 758 | "jmespath": { 759 | "version": "0.14.1", 760 | "resolved": "https://signiant.jfrog.io/signiant/api/npm/npm-virt/jmespath/-/jmespath-0.14.1.tgz", 761 | "integrity": "sha1-IdBjolyM5Oag6/xQC5kMXPDDTs4=", 762 | "dev": true 763 | }, 764 | "js-yaml": { 765 | "version": "3.13.1", 766 | "resolved": "https://signiant.jfrog.io/signiant/api/npm/npm-virt/js-yaml/-/js-yaml-3.13.1.tgz", 767 | "integrity": "sha1-r/FRswv9+o5J4F2iLnQV6d+jeEc=", 768 | "dev": true, 769 | "requires": { 770 | "argparse": "^1.0.7", 771 | "esprima": "^4.0.0" 772 | } 773 | }, 774 | "jsbn": { 775 | "version": "0.1.1", 776 | "resolved": "https://signiant.jfrog.io/signiant/api/npm/npm-virt/jsbn/-/jsbn-0.1.1.tgz", 777 | "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", 778 | "dev": true 779 | }, 780 | "jsmin": { 781 | "version": "1.0.1", 782 | "resolved": "https://signiant.jfrog.io/signiant/api/npm/npm-virt/jsmin/-/jsmin-1.0.1.tgz", 783 | "integrity": "sha1-570NzWSWw79IYyNb9GGj2YqjuYw=", 784 | "dev": true 785 | }, 786 | "json-schema": { 787 | "version": "0.2.3", 788 | "resolved": "https://signiant.jfrog.io/signiant/api/npm/npm-virt/json-schema/-/json-schema-0.2.3.tgz", 789 | "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=", 790 | "dev": true 791 | }, 792 | "json-schema-traverse": { 793 | "version": "0.4.1", 794 | "resolved": "https://signiant.jfrog.io/signiant/api/npm/npm-virt/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", 795 | "integrity": "sha1-afaofZUTq4u4/mO9sJecRI5oRmA=", 796 | "dev": true 797 | }, 798 | "json-stringify-safe": { 799 | "version": "5.0.1", 800 | "resolved": "https://signiant.jfrog.io/signiant/api/npm/npm-virt/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", 801 | "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=", 802 | "dev": true 803 | }, 804 | "jsonlint": { 805 | "version": "1.6.3", 806 | "resolved": "https://signiant.jfrog.io/signiant/api/npm/npm-virt/jsonlint/-/jsonlint-1.6.3.tgz", 807 | "integrity": "sha1-y14x78C3gpHQ2GL77wWQCt8hKYg=", 808 | "dev": true, 809 | "requires": { 810 | "JSV": "^4.0.x", 811 | "nomnom": "^1.5.x" 812 | } 813 | }, 814 | "jsprim": { 815 | "version": "1.4.1", 816 | "resolved": "https://signiant.jfrog.io/signiant/api/npm/npm-virt/jsprim/-/jsprim-1.4.1.tgz", 817 | "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", 818 | "dev": true, 819 | "requires": { 820 | "assert-plus": "1.0.0", 821 | "extsprintf": "1.3.0", 822 | "json-schema": "0.2.3", 823 | "verror": "1.10.0" 824 | } 825 | }, 826 | "load-json-file": { 827 | "version": "1.1.0", 828 | "resolved": "https://signiant.jfrog.io/signiant/api/npm/npm-virt/load-json-file/-/load-json-file-1.1.0.tgz", 829 | "integrity": "sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA=", 830 | "dev": true, 831 | "requires": { 832 | "graceful-fs": "^4.1.2", 833 | "parse-json": "^2.2.0", 834 | "pify": "^2.0.0", 835 | "pinkie-promise": "^2.0.0", 836 | "strip-bom": "^2.0.0" 837 | } 838 | }, 839 | "lodash": { 840 | "version": "4.17.15", 841 | "resolved": "https://signiant.jfrog.io/signiant/api/npm/npm-virt/lodash/-/lodash-4.17.15.tgz", 842 | "integrity": "sha1-tEf2ZwoEVbv+7dETku/zMOoJdUg=", 843 | "dev": true 844 | }, 845 | "loud-rejection": { 846 | "version": "1.6.0", 847 | "resolved": "https://signiant.jfrog.io/signiant/api/npm/npm-virt/loud-rejection/-/loud-rejection-1.6.0.tgz", 848 | "integrity": "sha1-W0b4AUft7leIcPCG0Eghz5mOVR8=", 849 | "dev": true, 850 | "requires": { 851 | "currently-unhandled": "^0.4.1", 852 | "signal-exit": "^3.0.0" 853 | } 854 | }, 855 | "map-obj": { 856 | "version": "1.0.1", 857 | "resolved": "https://signiant.jfrog.io/signiant/api/npm/npm-virt/map-obj/-/map-obj-1.0.1.tgz", 858 | "integrity": "sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0=", 859 | "dev": true 860 | }, 861 | "meow": { 862 | "version": "3.7.0", 863 | "resolved": "https://signiant.jfrog.io/signiant/api/npm/npm-virt/meow/-/meow-3.7.0.tgz", 864 | "integrity": "sha1-cstmi0JSKCkKu/qFaJJYcwioAfs=", 865 | "dev": true, 866 | "requires": { 867 | "camelcase-keys": "^2.0.0", 868 | "decamelize": "^1.1.2", 869 | "loud-rejection": "^1.0.0", 870 | "map-obj": "^1.0.1", 871 | "minimist": "^1.1.3", 872 | "normalize-package-data": "^2.3.4", 873 | "object-assign": "^4.0.1", 874 | "read-pkg-up": "^1.0.1", 875 | "redent": "^1.0.0", 876 | "trim-newlines": "^1.0.0" 877 | } 878 | }, 879 | "mime-db": { 880 | "version": "1.40.0", 881 | "resolved": "https://signiant.jfrog.io/signiant/api/npm/npm-virt/mime-db/-/mime-db-1.40.0.tgz", 882 | "integrity": "sha1-plBX6ZjbCQ9zKmj2wnbTh9QSbDI=", 883 | "dev": true 884 | }, 885 | "mime-types": { 886 | "version": "2.1.24", 887 | "resolved": "https://signiant.jfrog.io/signiant/api/npm/npm-virt/mime-types/-/mime-types-2.1.24.tgz", 888 | "integrity": "sha1-tvjQs+lR77d97eyhlM/20W9nb4E=", 889 | "dev": true, 890 | "requires": { 891 | "mime-db": "1.40.0" 892 | } 893 | }, 894 | "minimatch": { 895 | "version": "3.0.4", 896 | "resolved": "https://signiant.jfrog.io/signiant/api/npm/npm-virt/minimatch/-/minimatch-3.0.4.tgz", 897 | "integrity": "sha1-UWbihkV/AzBgZL5Ul+jbsMPTIIM=", 898 | "dev": true, 899 | "requires": { 900 | "brace-expansion": "^1.1.7" 901 | } 902 | }, 903 | "minimist": { 904 | "version": "1.2.0", 905 | "resolved": "https://signiant.jfrog.io/signiant/api/npm/npm-virt/minimist/-/minimist-1.2.0.tgz", 906 | "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", 907 | "dev": true 908 | }, 909 | "mkdirp": { 910 | "version": "0.5.1", 911 | "resolved": "https://signiant.jfrog.io/signiant/api/npm/npm-virt/mkdirp/-/mkdirp-0.5.1.tgz", 912 | "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", 913 | "dev": true, 914 | "requires": { 915 | "minimist": "0.0.8" 916 | }, 917 | "dependencies": { 918 | "minimist": { 919 | "version": "0.0.8", 920 | "resolved": "https://signiant.jfrog.io/signiant/api/npm/npm-virt/minimist/-/minimist-0.0.8.tgz", 921 | "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", 922 | "dev": true 923 | } 924 | } 925 | }, 926 | "nomnom": { 927 | "version": "1.8.1", 928 | "resolved": "https://signiant.jfrog.io/signiant/api/npm/npm-virt/nomnom/-/nomnom-1.8.1.tgz", 929 | "integrity": "sha1-IVH3Ikcrp55Qp2/BJbuMjy5Nwqc=", 930 | "dev": true, 931 | "requires": { 932 | "chalk": "~0.4.0", 933 | "underscore": "~1.6.0" 934 | } 935 | }, 936 | "nopt": { 937 | "version": "3.0.6", 938 | "resolved": "https://signiant.jfrog.io/signiant/api/npm/npm-virt/nopt/-/nopt-3.0.6.tgz", 939 | "integrity": "sha1-xkZdvwirzU2zWTF/eaxopkayj/k=", 940 | "dev": true, 941 | "requires": { 942 | "abbrev": "1" 943 | } 944 | }, 945 | "normalize-package-data": { 946 | "version": "2.5.0", 947 | "resolved": "https://signiant.jfrog.io/signiant/api/npm/npm-virt/normalize-package-data/-/normalize-package-data-2.5.0.tgz", 948 | "integrity": "sha1-5m2xg4sgDB38IzIl0SyzZSDiNKg=", 949 | "dev": true, 950 | "requires": { 951 | "hosted-git-info": "^2.1.4", 952 | "resolve": "^1.10.0", 953 | "semver": "2 || 3 || 4 || 5", 954 | "validate-npm-package-license": "^3.0.1" 955 | } 956 | }, 957 | "number-is-nan": { 958 | "version": "1.0.1", 959 | "resolved": "https://signiant.jfrog.io/signiant/api/npm/npm-virt/number-is-nan/-/number-is-nan-1.0.1.tgz", 960 | "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", 961 | "dev": true 962 | }, 963 | "oauth-sign": { 964 | "version": "0.9.0", 965 | "resolved": "https://signiant.jfrog.io/signiant/api/npm/npm-virt/oauth-sign/-/oauth-sign-0.9.0.tgz", 966 | "integrity": "sha1-R6ewFrqmi1+g7PPe4IqFxnmsZFU=", 967 | "dev": true 968 | }, 969 | "object-assign": { 970 | "version": "4.1.1", 971 | "resolved": "https://signiant.jfrog.io/signiant/api/npm/npm-virt/object-assign/-/object-assign-4.1.1.tgz", 972 | "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", 973 | "dev": true 974 | }, 975 | "once": { 976 | "version": "1.4.0", 977 | "resolved": "https://signiant.jfrog.io/signiant/api/npm/npm-virt/once/-/once-1.4.0.tgz", 978 | "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", 979 | "dev": true, 980 | "requires": { 981 | "wrappy": "1" 982 | } 983 | }, 984 | "parse-json": { 985 | "version": "2.2.0", 986 | "resolved": "https://signiant.jfrog.io/signiant/api/npm/npm-virt/parse-json/-/parse-json-2.2.0.tgz", 987 | "integrity": "sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=", 988 | "dev": true, 989 | "requires": { 990 | "error-ex": "^1.2.0" 991 | } 992 | }, 993 | "path-exists": { 994 | "version": "2.1.0", 995 | "resolved": "https://signiant.jfrog.io/signiant/api/npm/npm-virt/path-exists/-/path-exists-2.1.0.tgz", 996 | "integrity": "sha1-D+tsZPD8UY2adU3V77YscCJ2H0s=", 997 | "dev": true, 998 | "requires": { 999 | "pinkie-promise": "^2.0.0" 1000 | } 1001 | }, 1002 | "path-is-absolute": { 1003 | "version": "1.0.1", 1004 | "resolved": "https://signiant.jfrog.io/signiant/api/npm/npm-virt/path-is-absolute/-/path-is-absolute-1.0.1.tgz", 1005 | "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", 1006 | "dev": true 1007 | }, 1008 | "path-parse": { 1009 | "version": "1.0.7", 1010 | "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", 1011 | "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", 1012 | "dev": true 1013 | }, 1014 | "path-type": { 1015 | "version": "1.1.0", 1016 | "resolved": "https://signiant.jfrog.io/signiant/api/npm/npm-virt/path-type/-/path-type-1.1.0.tgz", 1017 | "integrity": "sha1-WcRPfuSR2nBNpBXaWkBwuk+P5EE=", 1018 | "dev": true, 1019 | "requires": { 1020 | "graceful-fs": "^4.1.2", 1021 | "pify": "^2.0.0", 1022 | "pinkie-promise": "^2.0.0" 1023 | } 1024 | }, 1025 | "performance-now": { 1026 | "version": "2.1.0", 1027 | "resolved": "https://signiant.jfrog.io/signiant/api/npm/npm-virt/performance-now/-/performance-now-2.1.0.tgz", 1028 | "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=", 1029 | "dev": true 1030 | }, 1031 | "pify": { 1032 | "version": "2.3.0", 1033 | "resolved": "https://signiant.jfrog.io/signiant/api/npm/npm-virt/pify/-/pify-2.3.0.tgz", 1034 | "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", 1035 | "dev": true 1036 | }, 1037 | "pinkie": { 1038 | "version": "2.0.4", 1039 | "resolved": "https://signiant.jfrog.io/signiant/api/npm/npm-virt/pinkie/-/pinkie-2.0.4.tgz", 1040 | "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA=", 1041 | "dev": true 1042 | }, 1043 | "pinkie-promise": { 1044 | "version": "2.0.1", 1045 | "resolved": "https://signiant.jfrog.io/signiant/api/npm/npm-virt/pinkie-promise/-/pinkie-promise-2.0.1.tgz", 1046 | "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=", 1047 | "dev": true, 1048 | "requires": { 1049 | "pinkie": "^2.0.0" 1050 | } 1051 | }, 1052 | "psl": { 1053 | "version": "1.4.0", 1054 | "resolved": "https://signiant.jfrog.io/signiant/api/npm/npm-virt/psl/-/psl-1.4.0.tgz", 1055 | "integrity": "sha1-XdJhVs22n6H9uKsZkWZ9P4DO18I=", 1056 | "dev": true 1057 | }, 1058 | "punycode": { 1059 | "version": "1.3.2", 1060 | "resolved": "https://signiant.jfrog.io/signiant/api/npm/npm-virt/punycode/-/punycode-1.3.2.tgz", 1061 | "integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=", 1062 | "dev": true 1063 | }, 1064 | "qs": { 1065 | "version": "6.5.2", 1066 | "resolved": "https://signiant.jfrog.io/signiant/api/npm/npm-virt/qs/-/qs-6.5.2.tgz", 1067 | "integrity": "sha1-yzroBuh0BERYTvFUzo7pjUA/PjY=", 1068 | "dev": true 1069 | }, 1070 | "querystring": { 1071 | "version": "0.2.0", 1072 | "resolved": "https://signiant.jfrog.io/signiant/api/npm/npm-virt/querystring/-/querystring-0.2.0.tgz", 1073 | "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=", 1074 | "dev": true 1075 | }, 1076 | "read-pkg": { 1077 | "version": "1.1.0", 1078 | "resolved": "https://signiant.jfrog.io/signiant/api/npm/npm-virt/read-pkg/-/read-pkg-1.1.0.tgz", 1079 | "integrity": "sha1-9f+qXs0pyzHAR0vKfXVra7KePyg=", 1080 | "dev": true, 1081 | "requires": { 1082 | "load-json-file": "^1.0.0", 1083 | "normalize-package-data": "^2.3.2", 1084 | "path-type": "^1.0.0" 1085 | } 1086 | }, 1087 | "read-pkg-up": { 1088 | "version": "1.0.1", 1089 | "resolved": "https://signiant.jfrog.io/signiant/api/npm/npm-virt/read-pkg-up/-/read-pkg-up-1.0.1.tgz", 1090 | "integrity": "sha1-nWPBMnbAZZGNV/ACpX9AobZD+wI=", 1091 | "dev": true, 1092 | "requires": { 1093 | "find-up": "^1.0.0", 1094 | "read-pkg": "^1.0.0" 1095 | } 1096 | }, 1097 | "redent": { 1098 | "version": "1.0.0", 1099 | "resolved": "https://signiant.jfrog.io/signiant/api/npm/npm-virt/redent/-/redent-1.0.0.tgz", 1100 | "integrity": "sha1-z5Fqsf1fHxbfsggi3W7H9zDCr94=", 1101 | "dev": true, 1102 | "requires": { 1103 | "indent-string": "^2.1.0", 1104 | "strip-indent": "^1.0.1" 1105 | } 1106 | }, 1107 | "repeating": { 1108 | "version": "2.0.1", 1109 | "resolved": "https://signiant.jfrog.io/signiant/api/npm/npm-virt/repeating/-/repeating-2.0.1.tgz", 1110 | "integrity": "sha1-UhTFOpJtNVJwdSf7q0FdvAjQbdo=", 1111 | "dev": true, 1112 | "requires": { 1113 | "is-finite": "^1.0.0" 1114 | } 1115 | }, 1116 | "request": { 1117 | "version": "2.88.0", 1118 | "resolved": "https://signiant.jfrog.io/signiant/api/npm/npm-virt/request/-/request-2.88.0.tgz", 1119 | "integrity": "sha1-nC/KT301tZLv5Xx/ClXoEFIST+8=", 1120 | "dev": true, 1121 | "requires": { 1122 | "aws-sign2": "~0.7.0", 1123 | "aws4": "^1.8.0", 1124 | "caseless": "~0.12.0", 1125 | "combined-stream": "~1.0.6", 1126 | "extend": "~3.0.2", 1127 | "forever-agent": "~0.6.1", 1128 | "form-data": "~2.3.2", 1129 | "har-validator": "~5.1.0", 1130 | "http-signature": "~1.2.0", 1131 | "is-typedarray": "~1.0.0", 1132 | "isstream": "~0.1.2", 1133 | "json-stringify-safe": "~5.0.1", 1134 | "mime-types": "~2.1.19", 1135 | "oauth-sign": "~0.9.0", 1136 | "performance-now": "^2.1.0", 1137 | "qs": "~6.5.2", 1138 | "safe-buffer": "^5.1.2", 1139 | "tough-cookie": "~2.4.3", 1140 | "tunnel-agent": "^0.6.0", 1141 | "uuid": "^3.3.2" 1142 | } 1143 | }, 1144 | "resolve": { 1145 | "version": "1.12.0", 1146 | "resolved": "https://signiant.jfrog.io/signiant/api/npm/npm-virt/resolve/-/resolve-1.12.0.tgz", 1147 | "integrity": "sha1-P8ZEo1yEpIVUYJ/ybsUrZvpXffY=", 1148 | "dev": true, 1149 | "requires": { 1150 | "path-parse": "^1.0.6" 1151 | } 1152 | }, 1153 | "rimraf": { 1154 | "version": "2.6.3", 1155 | "resolved": "https://signiant.jfrog.io/signiant/api/npm/npm-virt/rimraf/-/rimraf-2.6.3.tgz", 1156 | "integrity": "sha1-stEE/g2Psnz54KHNqCYt04M8bKs=", 1157 | "dev": true, 1158 | "requires": { 1159 | "glob": "^7.1.3" 1160 | }, 1161 | "dependencies": { 1162 | "glob": { 1163 | "version": "7.1.5", 1164 | "resolved": "https://signiant.jfrog.io/signiant/api/npm/npm-virt/glob/-/glob-7.1.5.tgz", 1165 | "integrity": "sha1-ZxTGm+4g88PmTE3ZBVU+UytAzcA=", 1166 | "dev": true, 1167 | "requires": { 1168 | "fs.realpath": "^1.0.0", 1169 | "inflight": "^1.0.4", 1170 | "inherits": "2", 1171 | "minimatch": "^3.0.4", 1172 | "once": "^1.3.0", 1173 | "path-is-absolute": "^1.0.0" 1174 | } 1175 | } 1176 | } 1177 | }, 1178 | "safe-buffer": { 1179 | "version": "5.2.0", 1180 | "resolved": "https://signiant.jfrog.io/signiant/api/npm/npm-virt/safe-buffer/-/safe-buffer-5.2.0.tgz", 1181 | "integrity": "sha1-t02uxJsRSPiMZLaNSbHoFcHy9Rk=", 1182 | "dev": true 1183 | }, 1184 | "safer-buffer": { 1185 | "version": "2.1.2", 1186 | "resolved": "https://signiant.jfrog.io/signiant/api/npm/npm-virt/safer-buffer/-/safer-buffer-2.1.2.tgz", 1187 | "integrity": "sha1-RPoWGwGHuVSd2Eu5GAL5vYOFzWo=", 1188 | "dev": true 1189 | }, 1190 | "sax": { 1191 | "version": "1.2.1", 1192 | "resolved": "https://signiant.jfrog.io/signiant/api/npm/npm-virt/sax/-/sax-1.2.1.tgz", 1193 | "integrity": "sha1-e45lYZCyKOgaZq6nSEgNgozS03o=", 1194 | "dev": true 1195 | }, 1196 | "semver": { 1197 | "version": "5.7.1", 1198 | "resolved": "https://signiant.jfrog.io/signiant/api/npm/npm-virt/semver/-/semver-5.7.1.tgz", 1199 | "integrity": "sha1-qVT5Ma66UI0we78Gnv8MAclhFvc=", 1200 | "dev": true 1201 | }, 1202 | "signal-exit": { 1203 | "version": "3.0.2", 1204 | "resolved": "https://signiant.jfrog.io/signiant/api/npm/npm-virt/signal-exit/-/signal-exit-3.0.2.tgz", 1205 | "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=", 1206 | "dev": true 1207 | }, 1208 | "source-map-support": { 1209 | "version": "0.5.16", 1210 | "resolved": "https://signiant.jfrog.io/signiant/api/npm/npm-virt/source-map-support/-/source-map-support-0.5.16.tgz", 1211 | "integrity": "sha1-CuBp5/47p1OMZMmFFeNTOerFoEI=", 1212 | "dev": true, 1213 | "requires": { 1214 | "buffer-from": "^1.0.0", 1215 | "source-map": "^0.6.0" 1216 | }, 1217 | "dependencies": { 1218 | "source-map": { 1219 | "version": "0.6.1", 1220 | "resolved": "https://signiant.jfrog.io/signiant/api/npm/npm-virt/source-map/-/source-map-0.6.1.tgz", 1221 | "integrity": "sha1-dHIq8y6WFOnCh6jQu95IteLxomM=", 1222 | "dev": true 1223 | } 1224 | } 1225 | }, 1226 | "spdx-correct": { 1227 | "version": "3.1.0", 1228 | "resolved": "https://signiant.jfrog.io/signiant/api/npm/npm-virt/spdx-correct/-/spdx-correct-3.1.0.tgz", 1229 | "integrity": "sha1-+4PlBERSaPFUsHTiGMh8ADzTHfQ=", 1230 | "dev": true, 1231 | "requires": { 1232 | "spdx-expression-parse": "^3.0.0", 1233 | "spdx-license-ids": "^3.0.0" 1234 | } 1235 | }, 1236 | "spdx-exceptions": { 1237 | "version": "2.2.0", 1238 | "resolved": "https://signiant.jfrog.io/signiant/api/npm/npm-virt/spdx-exceptions/-/spdx-exceptions-2.2.0.tgz", 1239 | "integrity": "sha1-LqRQrudPKom/uUUZwH/Nb0EyKXc=", 1240 | "dev": true 1241 | }, 1242 | "spdx-expression-parse": { 1243 | "version": "3.0.0", 1244 | "resolved": "https://signiant.jfrog.io/signiant/api/npm/npm-virt/spdx-expression-parse/-/spdx-expression-parse-3.0.0.tgz", 1245 | "integrity": "sha1-meEZt6XaAOBUkcn6M4t5BII7QdA=", 1246 | "dev": true, 1247 | "requires": { 1248 | "spdx-exceptions": "^2.1.0", 1249 | "spdx-license-ids": "^3.0.0" 1250 | } 1251 | }, 1252 | "spdx-license-ids": { 1253 | "version": "3.0.5", 1254 | "resolved": "https://signiant.jfrog.io/signiant/api/npm/npm-virt/spdx-license-ids/-/spdx-license-ids-3.0.5.tgz", 1255 | "integrity": "sha1-NpS1gEVnpFjTyARYQqY1hjL2JlQ=", 1256 | "dev": true 1257 | }, 1258 | "sprintf-js": { 1259 | "version": "1.1.2", 1260 | "resolved": "https://signiant.jfrog.io/signiant/api/npm/npm-virt/sprintf-js/-/sprintf-js-1.1.2.tgz", 1261 | "integrity": "sha1-2hdlJiv4wPVxdJ8q1sJjACB65nM=", 1262 | "dev": true 1263 | }, 1264 | "sshpk": { 1265 | "version": "1.16.1", 1266 | "resolved": "https://signiant.jfrog.io/signiant/api/npm/npm-virt/sshpk/-/sshpk-1.16.1.tgz", 1267 | "integrity": "sha1-+2YcC+8ps520B2nuOfpwCT1vaHc=", 1268 | "dev": true, 1269 | "requires": { 1270 | "asn1": "~0.2.3", 1271 | "assert-plus": "^1.0.0", 1272 | "bcrypt-pbkdf": "^1.0.0", 1273 | "dashdash": "^1.12.0", 1274 | "ecc-jsbn": "~0.1.1", 1275 | "getpass": "^0.1.1", 1276 | "jsbn": "~0.1.0", 1277 | "safer-buffer": "^2.0.2", 1278 | "tweetnacl": "~0.14.0" 1279 | } 1280 | }, 1281 | "strip-ansi": { 1282 | "version": "0.1.1", 1283 | "resolved": "https://signiant.jfrog.io/signiant/api/npm/npm-virt/strip-ansi/-/strip-ansi-0.1.1.tgz", 1284 | "integrity": "sha1-OeipjQRNFQZgq+SmgIrPcLt7yZE=", 1285 | "dev": true 1286 | }, 1287 | "strip-bom": { 1288 | "version": "2.0.0", 1289 | "resolved": "https://signiant.jfrog.io/signiant/api/npm/npm-virt/strip-bom/-/strip-bom-2.0.0.tgz", 1290 | "integrity": "sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4=", 1291 | "dev": true, 1292 | "requires": { 1293 | "is-utf8": "^0.2.0" 1294 | } 1295 | }, 1296 | "strip-indent": { 1297 | "version": "1.0.1", 1298 | "resolved": "https://signiant.jfrog.io/signiant/api/npm/npm-virt/strip-indent/-/strip-indent-1.0.1.tgz", 1299 | "integrity": "sha1-DHlipq3vp7vUrDZkYKY4VSrhoKI=", 1300 | "dev": true, 1301 | "requires": { 1302 | "get-stdin": "^4.0.1" 1303 | } 1304 | }, 1305 | "supports-color": { 1306 | "version": "5.5.0", 1307 | "resolved": "https://signiant.jfrog.io/signiant/api/npm/npm-virt/supports-color/-/supports-color-5.5.0.tgz", 1308 | "integrity": "sha1-4uaaRKyHcveKHsCzW2id9lMO/I8=", 1309 | "dev": true, 1310 | "requires": { 1311 | "has-flag": "^3.0.0" 1312 | } 1313 | }, 1314 | "terser": { 1315 | "version": "4.4.2", 1316 | "resolved": "https://signiant.jfrog.io/signiant/api/npm/npm-virt/terser/-/terser-4.4.2.tgz", 1317 | "integrity": "sha1-RI//rQJF9Miid86JeItFi/13Bug=", 1318 | "dev": true, 1319 | "requires": { 1320 | "commander": "^2.20.0", 1321 | "source-map": "~0.6.1", 1322 | "source-map-support": "~0.5.12" 1323 | }, 1324 | "dependencies": { 1325 | "source-map": { 1326 | "version": "0.6.1", 1327 | "resolved": "https://signiant.jfrog.io/signiant/api/npm/npm-virt/source-map/-/source-map-0.6.1.tgz", 1328 | "integrity": "sha1-dHIq8y6WFOnCh6jQu95IteLxomM=", 1329 | "dev": true 1330 | } 1331 | } 1332 | }, 1333 | "tough-cookie": { 1334 | "version": "2.4.3", 1335 | "resolved": "https://signiant.jfrog.io/signiant/api/npm/npm-virt/tough-cookie/-/tough-cookie-2.4.3.tgz", 1336 | "integrity": "sha1-U/Nto/R3g7CSWvoG/587FlKA94E=", 1337 | "dev": true, 1338 | "requires": { 1339 | "psl": "^1.1.24", 1340 | "punycode": "^1.4.1" 1341 | }, 1342 | "dependencies": { 1343 | "punycode": { 1344 | "version": "1.4.1", 1345 | "resolved": "https://signiant.jfrog.io/signiant/api/npm/npm-virt/punycode/-/punycode-1.4.1.tgz", 1346 | "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=", 1347 | "dev": true 1348 | } 1349 | } 1350 | }, 1351 | "trim-newlines": { 1352 | "version": "1.0.0", 1353 | "resolved": "https://signiant.jfrog.io/signiant/api/npm/npm-virt/trim-newlines/-/trim-newlines-1.0.0.tgz", 1354 | "integrity": "sha1-WIeWa7WCpFA6QetST301ARgVphM=", 1355 | "dev": true 1356 | }, 1357 | "tunnel-agent": { 1358 | "version": "0.6.0", 1359 | "resolved": "https://signiant.jfrog.io/signiant/api/npm/npm-virt/tunnel-agent/-/tunnel-agent-0.6.0.tgz", 1360 | "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", 1361 | "dev": true, 1362 | "requires": { 1363 | "safe-buffer": "^5.0.1" 1364 | } 1365 | }, 1366 | "tweetnacl": { 1367 | "version": "0.14.5", 1368 | "resolved": "https://signiant.jfrog.io/signiant/api/npm/npm-virt/tweetnacl/-/tweetnacl-0.14.5.tgz", 1369 | "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", 1370 | "dev": true 1371 | }, 1372 | "underscore": { 1373 | "version": "1.6.0", 1374 | "resolved": "https://signiant.jfrog.io/signiant/api/npm/npm-virt/underscore/-/underscore-1.6.0.tgz", 1375 | "integrity": "sha1-izixDKze9jM3uLJOT/htRa6lKag=", 1376 | "dev": true 1377 | }, 1378 | "underscore.string": { 1379 | "version": "3.3.5", 1380 | "resolved": "https://signiant.jfrog.io/signiant/api/npm/npm-virt/underscore.string/-/underscore.string-3.3.5.tgz", 1381 | "integrity": "sha1-/CrSVbi9MJ4jnLxYFv0jqbfqQCM=", 1382 | "dev": true, 1383 | "requires": { 1384 | "sprintf-js": "^1.0.3", 1385 | "util-deprecate": "^1.0.2" 1386 | } 1387 | }, 1388 | "uri-js": { 1389 | "version": "4.2.2", 1390 | "resolved": "https://signiant.jfrog.io/signiant/api/npm/npm-virt/uri-js/-/uri-js-4.2.2.tgz", 1391 | "integrity": "sha1-lMVA4f93KVbiKZUHwBCupsiDjrA=", 1392 | "dev": true, 1393 | "requires": { 1394 | "punycode": "^2.1.0" 1395 | }, 1396 | "dependencies": { 1397 | "punycode": { 1398 | "version": "2.1.1", 1399 | "resolved": "https://signiant.jfrog.io/signiant/api/npm/npm-virt/punycode/-/punycode-2.1.1.tgz", 1400 | "integrity": "sha1-tYsBCsQMIsVldhbI0sLALHv0eew=", 1401 | "dev": true 1402 | } 1403 | } 1404 | }, 1405 | "url": { 1406 | "version": "0.10.3", 1407 | "resolved": "https://signiant.jfrog.io/signiant/api/npm/npm-virt/url/-/url-0.10.3.tgz", 1408 | "integrity": "sha1-Ah5NnHcF8hu/N9A861h2dAJ3TGQ=", 1409 | "dev": true, 1410 | "requires": { 1411 | "punycode": "1.3.2", 1412 | "querystring": "0.2.0" 1413 | } 1414 | }, 1415 | "util-deprecate": { 1416 | "version": "1.0.2", 1417 | "resolved": "https://signiant.jfrog.io/signiant/api/npm/npm-virt/util-deprecate/-/util-deprecate-1.0.2.tgz", 1418 | "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", 1419 | "dev": true 1420 | }, 1421 | "uuid": { 1422 | "version": "3.3.2", 1423 | "resolved": "https://signiant.jfrog.io/signiant/api/npm/npm-virt/uuid/-/uuid-3.3.2.tgz", 1424 | "integrity": "sha1-G0r0lV6zB3xQHCOHL8ZROBFYcTE=", 1425 | "dev": true 1426 | }, 1427 | "validate-npm-package-license": { 1428 | "version": "3.0.4", 1429 | "resolved": "https://signiant.jfrog.io/signiant/api/npm/npm-virt/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", 1430 | "integrity": "sha1-/JH2uce6FchX9MssXe/uw51PQQo=", 1431 | "dev": true, 1432 | "requires": { 1433 | "spdx-correct": "^3.0.0", 1434 | "spdx-expression-parse": "^3.0.0" 1435 | } 1436 | }, 1437 | "verror": { 1438 | "version": "1.10.0", 1439 | "resolved": "https://signiant.jfrog.io/signiant/api/npm/npm-virt/verror/-/verror-1.10.0.tgz", 1440 | "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", 1441 | "dev": true, 1442 | "requires": { 1443 | "assert-plus": "^1.0.0", 1444 | "core-util-is": "1.0.2", 1445 | "extsprintf": "^1.2.0" 1446 | } 1447 | }, 1448 | "which": { 1449 | "version": "1.3.1", 1450 | "resolved": "https://signiant.jfrog.io/signiant/api/npm/npm-virt/which/-/which-1.3.1.tgz", 1451 | "integrity": "sha1-pFBD1U9YBTFtqNYvn1CRjT2nCwo=", 1452 | "dev": true, 1453 | "requires": { 1454 | "isexe": "^2.0.0" 1455 | } 1456 | }, 1457 | "wrappy": { 1458 | "version": "1.0.2", 1459 | "resolved": "https://signiant.jfrog.io/signiant/api/npm/npm-virt/wrappy/-/wrappy-1.0.2.tgz", 1460 | "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", 1461 | "dev": true 1462 | }, 1463 | "xml2js": { 1464 | "version": "0.4.19", 1465 | "resolved": "https://signiant.jfrog.io/signiant/api/npm/npm-virt/xml2js/-/xml2js-0.4.19.tgz", 1466 | "integrity": "sha1-aGwg8hMgnpSr8NG88e+qKRx4J6c=", 1467 | "dev": true, 1468 | "requires": { 1469 | "sax": ">=0.6.0", 1470 | "xmlbuilder": "~9.0.1" 1471 | } 1472 | }, 1473 | "xmlbuilder": { 1474 | "version": "9.0.7", 1475 | "resolved": "https://signiant.jfrog.io/signiant/api/npm/npm-virt/xmlbuilder/-/xmlbuilder-9.0.7.tgz", 1476 | "integrity": "sha1-Ey7mPS7FVlxVfiD0wi35rKaGsQ0=", 1477 | "dev": true 1478 | } 1479 | } 1480 | } 1481 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dynamodb-replication", 3 | "description": "A lambda backed dynamodb cross region replication solution", 4 | "version": "1.0.0", 5 | "author": "Jonathan Seed ", 6 | "engines": { 7 | "node": ">=10" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/Signiant/dynamodb-replication" 12 | }, 13 | "keywords": [ 14 | "dynamodb", 15 | "lambda", 16 | "replication" 17 | ], 18 | "scripts": { 19 | "build": "grunt" 20 | }, 21 | "license": "MIT", 22 | "devDependencies": { 23 | "cfn-include": "^0.6.3", 24 | "grunt": "^1.0.1", 25 | "grunt-contrib-clean": "^1.0.0", 26 | "grunt-terser": "^1.0.0" 27 | } 28 | } 29 | --------------------------------------------------------------------------------