From d3fa248795d5d672f3e4574b69cfa47daf6d4c3f Mon Sep 17 00:00:00 2001 From: James Mead <3169+floehopper@users.noreply.github.com> Date: Fri, 14 Dec 2018 22:29:22 +0000 Subject: [PATCH 01/57] Remove files/content related to Ruby/Dropbox version of project --- .gitignore | 2 +- Capfile | 23 ----------- Gemfile | 14 ------- Gemfile.lock | 97 ---------------------------------------------- README.md | 82 --------------------------------------- backup.rb | 56 -------------------------- config/schedule.rb | 6 --- 7 files changed, 1 insertion(+), 279 deletions(-) delete mode 100644 Capfile delete mode 100644 Gemfile delete mode 100644 Gemfile.lock delete mode 100755 backup.rb delete mode 100644 config/schedule.rb diff --git a/.gitignore b/.gitignore index 4c49bd7..8b13789 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1 @@ -.env + diff --git a/Capfile b/Capfile deleted file mode 100644 index 05237b9..0000000 --- a/Capfile +++ /dev/null @@ -1,23 +0,0 @@ -require 'recap/recipes/ruby' - -set :application, 'trello_backup' -set :repository, 'https://github.com/freerange/trello_backup' - -server 'badger.gofreerange.com', :app - - -set :whenever_command, 'bundle exec whenever' -set(:whenever_identifier) { application } -set(:whenever_update_flags) { "--update-crontab #{whenever_identifier} -u #{application_user}" } -set(:whenever_clear_flags) { "--clear-crontab #{whenever_identifier} -u #{application_user}" } - -after "deploy:tag", "whenever:update_crontab" -after "deploy:rollback", "whenever:update_crontab" - -namespace :whenever do - desc "Update application's crontab entries using Whenever" - task :update_crontab, :roles => :app do - as_app "#{whenever_command} #{whenever_clear_flags}" - as_app "#{whenever_command} #{whenever_update_flags}" - end -end diff --git a/Gemfile b/Gemfile deleted file mode 100644 index 445bfdd..0000000 --- a/Gemfile +++ /dev/null @@ -1,14 +0,0 @@ -source "https://rubygems.org" - -ruby '2.2.10' - -gem "dotenv" -gem "addressable" -gem "rest-client" -gem "dropbox-sdk-v2" -gem "whenever" -gem "recap" -gem "ruby-trello" - -gem "activesupport" -gem "mime-types" diff --git a/Gemfile.lock b/Gemfile.lock deleted file mode 100644 index 098ae97..0000000 --- a/Gemfile.lock +++ /dev/null @@ -1,97 +0,0 @@ -GEM - remote: https://rubygems.org/ - specs: - activemodel (5.2.1) - activesupport (= 5.2.1) - activesupport (5.2.1) - concurrent-ruby (~> 1.0, >= 1.0.2) - i18n (>= 0.7, < 2) - minitest (~> 5.1) - tzinfo (~> 1.1) - addressable (2.5.2) - public_suffix (>= 2.0.2, < 4.0) - capistrano (2.15.5) - highline - net-scp (>= 1.0.0) - net-sftp (>= 2.0.0) - net-ssh (>= 2.0.14) - net-ssh-gateway (>= 1.1.0) - chronic (0.10.2) - concurrent-ruby (1.0.5) - domain_name (0.5.20170404) - unf (>= 0.0.5, < 1.0.0) - dotenv (0.11.1) - dotenv-deployment (~> 0.0.2) - dotenv-deployment (0.0.2) - dropbox-sdk-v2 (0.0.3) - http (~> 2.0) - highline (1.6.21) - http (2.2.2) - addressable (~> 2.3) - http-cookie (~> 1.0) - http-form_data (~> 1.0.1) - http_parser.rb (~> 0.6.0) - http-cookie (1.0.3) - domain_name (~> 0.5) - http-form_data (1.0.3) - http_parser.rb (0.6.0) - i18n (1.1.1) - concurrent-ruby (~> 1.0) - json (2.1.0) - mime-types (2.99.3) - minitest (5.11.3) - net-scp (1.2.1) - net-ssh (>= 2.6.5) - net-sftp (2.1.2) - net-ssh (>= 2.6.5) - net-ssh (2.9.1) - net-ssh-gateway (1.2.0) - net-ssh (>= 2.6.5) - netrc (0.11.0) - oauth (0.5.4) - open4 (1.3.4) - public_suffix (3.0.3) - recap (1.2.2) - capistrano (~> 2.9) - open4 - thor - rest-client (1.8.0) - http-cookie (>= 1.0.2, < 2.0) - mime-types (>= 1.16, < 3.0) - netrc (~> 0.7) - ruby-trello (1.5.1) - activemodel (>= 3.2.0) - addressable (~> 2.3) - json - oauth (>= 0.4.5) - rest-client (~> 1.8.0) - thor (0.19.1) - thread_safe (0.3.6) - tzinfo (1.2.5) - thread_safe (~> 0.1) - unf (0.1.4) - unf_ext - unf_ext (0.0.7.4) - whenever (0.9.2) - activesupport (>= 2.3.4) - chronic (>= 0.6.3) - -PLATFORMS - ruby - -DEPENDENCIES - activesupport - addressable - dotenv - dropbox-sdk-v2 - mime-types - recap - rest-client - ruby-trello - whenever - -RUBY VERSION - ruby 2.2.10p489 - -BUNDLED WITH - 1.16.6 diff --git a/README.md b/README.md index 533d3c5..e69de29 100644 --- a/README.md +++ b/README.md @@ -1,82 +0,0 @@ -We use this script to regularly backup all our GFR Trello boards to Dropbox. - -## Generating/storing the tokens you need to use this script - -### Getting the Trello API key - -Ensure you're logged in as the GFR Admin user. - - $ open "https://trello.com/1/appKey/generate" - # Copy the Key (under Developer API Keys) to the clipboard - - # Temporarily store the Trello API key in an environment variable - $ export TRELLO_KEY=`pbpaste` - - # Store the Trello API key in .env - $ echo "TRELLO_KEY=$TRELLO_KEY" >> .env - -### Getting a token to allow this app to read our Trello board - -Ensure you're logged in as the GFR Admin user. It's safe to run do this step multiple times as you'll get the same token back even if it's already been generated. - - $ open "https://trello.com/1/connect?key=$TRELLO_KEY&name=gfr-trello-backup&expiration=never&response_type=token" - # Allow the gfr-trello-backup app to read our Trello account - - # Copy the token from the resulting page - - # Temporarily store the Trello token in an environment variable - $ export TRELLO_TOKEN=`pbpaste` - - # Store the Trello token in .env - $ echo "TRELLO_TOKEN=$TRELLO_TOKEN" >> .env - -### Getting a token to allow this app to write to Dropbox - -Ensure you're logged in as the GFR Admin user. - - $ open "https://www.dropbox.com/developers/apps" - - # Navigate to "Trello Backup" - # Click the "Generate" button in OAuth 2 > Generated access token - # Copy the generated access token - - # Temporarily store the access token in an environment variable - $ export DROPBOX_ACCESS_TOKEN=`pbpaste` - - # Store the Dropbox token in .env - $ echo "DROPBOX_ACCESS_TOKEN=$DROPBOX_ACCESS_TOKEN" >> .env - -## Testing the script locally - -Assuming you've followed the instructions above and got all the tokens stored in .env, this should be as simple as running `ruby backup.rb`. If it's worked successfully then you'll see a backup for today in https://www.dropbox.com/home/Apps/Trello%20Backup. - -## Deployment - -### Deploying with recap - - $ cap bootstrap - $ cap deploy:setup - $ cap deploy - -### Set the application user's shell to bash - - root$ chsh -s /bin/bash trello_backup - -### Set the Trello and Dropbox environment variables - -I'm going to assume you've already got these environment variables configured in your local .env. - - $ cap env:set TRELLO_KEY=`grep TRELLO_KEY .env | cut -d"=" -f2` - $ cap env:set TRELLO_TOKEN=`grep TRELLO_TOKEN .env | cut -d"=" -f2` - $ cap env:set DROPBOX_ACCESS_TOKEN=`grep DROPBOX_ACCESS_TOKEN .env | cut -d"=" -f2` - -## Test the script by running it manually - - # Print the command that's being run under cron - trello_backup$ crontab -l | grep -o "/bin/bash.*backup.rb.*" - - # Copy and paste the printed command to execute the backup script manually - # If everything has worked then you should see a new backup file in - # https://www.dropbox.com/home/Apps/Trello%20Backup when you're logged - # in as GFR's dropbox user. - diff --git a/backup.rb b/backup.rb deleted file mode 100755 index 4ee0f65..0000000 --- a/backup.rb +++ /dev/null @@ -1,56 +0,0 @@ -require 'logger' -logger = Logger.new(STDOUT) -logger.formatter = Logger::Formatter.new - -STDOUT.sync = true - -logger.info 'Loading environment...' - -require 'rubygems' -require 'bundler/setup' - -require 'dotenv' -require 'addressable/uri' -require 'restclient' -require 'dropbox' -require 'date' -require 'trello' - -Dotenv.load - -logger.info 'OK' - -Trello.configure do |config| - config.developer_public_key = ENV.fetch('TRELLO_KEY') - config.member_token = ENV.fetch('TRELLO_TOKEN') -end - -boards = Trello::Board.all.reject { |b| b.name[/Welcome Board/] } -boards.each do |board| - endpoint = "https://api.trello.com/1/boards/#{board.id}" - uri = Addressable::URI.parse(endpoint) - uri.query_values = { - :actions => :all, - :actions_limit => 1000, - :cards => :all, - :lists => :all, - :members => :all, - :member_fields => :all, - :checklists => :all, - :fields => :all, - :card_attachments => true, - :key => ENV.fetch('TRELLO_KEY'), - :token => ENV.fetch('TRELLO_TOKEN') - } - - logger.info "#{board.id} - Fetching Trello data for #{board.name} board..." - response = RestClient.get(uri.to_s) - json = response.body - logger.info "#{board.id} - OK" - - logger.info "#{board.id} - Writing data to Dropbox..." - client = Dropbox::Client.new(ENV['DROPBOX_ACCESS_TOKEN']) - filename = "/#{Date.today}-#{board.name.downcase.gsub(/[^\w]/, '-').squeeze('-')}-trello.json" - client.upload(filename, json) - logger.info "#{board.id} - OK" -end diff --git a/config/schedule.rb b/config/schedule.rb deleted file mode 100644 index e7f0385..0000000 --- a/config/schedule.rb +++ /dev/null @@ -1,6 +0,0 @@ -env :PATH, '/usr/local/bin:/usr/bin:/bin' -env :MAILTO, 'everyone@gofreerange.com' - -every :day, :at => '2am' do - command 'cd ~/app && ruby backup.rb >> trello_backup.log 2>&1 || echo "Trello Backup: error occurred. See trello_backup.log for details."' -end From b8dfe11722bab872ca31fa42b6268303080f8234 Mon Sep 17 00:00:00 2001 From: James Mead <3169+floehopper@users.noreply.github.com> Date: Fri, 14 Dec 2018 21:56:47 +0000 Subject: [PATCH 02/57] Add basic package.json And run `npm install` to generate `package-lock.json`. --- README.md | 8 ++++++++ package-lock.json | 5 +++++ package.json | 8 ++++++++ 3 files changed, 21 insertions(+) create mode 100644 package-lock.json create mode 100644 package.json diff --git a/README.md b/README.md index e69de29..676bc38 100644 --- a/README.md +++ b/README.md @@ -0,0 +1,8 @@ +## Requirements + +* node +* npm + +## Setup + + npm ci diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..879b452 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,5 @@ +{ + "name": "trello_backup", + "version": "1.0.0", + "lockfileVersion": 1 +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..df544e9 --- /dev/null +++ b/package.json @@ -0,0 +1,8 @@ +{ + "name": "trello_backup", + "version": "1.0.0", + "description": "Backup Trello boards to S3", + "main": "index.js", + "author": "Go Free Range Ltd", + "license": "ISC" +} From 0cbe3f064c1c25b3ab1b663ff6a55634a8d8a5b9 Mon Sep 17 00:00:00 2001 From: James Mead <3169+floehopper@users.noreply.github.com> Date: Fri, 14 Dec 2018 22:03:48 +0000 Subject: [PATCH 03/57] Add typescript package --- package-lock.json | 10 +++++++++- package.json | 5 ++++- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 879b452..51dcde4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,5 +1,13 @@ { "name": "trello_backup", "version": "1.0.0", - "lockfileVersion": 1 + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "typescript": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.2.2.tgz", + "integrity": "sha512-VCj5UiSyHBjwfYacmDuc/NOk4QQixbE+Wn7MFJuS0nRuPQbof132Pw4u53dm264O8LPc2MVsc7RJNml5szurkg==" + } + } } diff --git a/package.json b/package.json index df544e9..1cb7242 100644 --- a/package.json +++ b/package.json @@ -4,5 +4,8 @@ "description": "Backup Trello boards to S3", "main": "index.js", "author": "Go Free Range Ltd", - "license": "ISC" + "license": "ISC", + "dependencies": { + "typescript": "^3.2.2" + } } From 51ecb74d9d22eaa46c3c3befc7a1a486c206352e Mon Sep 17 00:00:00 2001 From: James Mead <3169+floehopper@users.noreply.github.com> Date: Fri, 14 Dec 2018 22:06:03 +0000 Subject: [PATCH 04/57] Add build script to compile index.ts to index.js --- .gitignore | 2 +- README.md | 4 ++++ index.ts | 4 ++++ package.json | 3 +++ tsconfig.json | 6 ++++++ 5 files changed, 18 insertions(+), 1 deletion(-) create mode 100644 index.ts create mode 100644 tsconfig.json diff --git a/.gitignore b/.gitignore index 8b13789..012a3cd 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1 @@ - +index.js diff --git a/README.md b/README.md index 676bc38..356a734 100644 --- a/README.md +++ b/README.md @@ -6,3 +6,7 @@ ## Setup npm ci + +## Build + + npm run build diff --git a/index.ts b/index.ts new file mode 100644 index 0000000..b672f02 --- /dev/null +++ b/index.ts @@ -0,0 +1,4 @@ +class TrelloBackupStack { +} + +new TrelloBackupStack(); diff --git a/package.json b/package.json index 1cb7242..c3a2c93 100644 --- a/package.json +++ b/package.json @@ -3,6 +3,9 @@ "version": "1.0.0", "description": "Backup Trello boards to S3", "main": "index.js", + "scripts": { + "build": "tsc" + }, "author": "Go Free Range Ltd", "license": "ISC", "dependencies": { diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..d68129f --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,6 @@ +{ + "compilerOptions": { + "target": "es2018", + "module": "commonjs" + } +} From 223aa99670fb2246c41275afeb7f62b671e3b3c5 Mon Sep 17 00:00:00 2001 From: James Mead <3169+floehopper@users.noreply.github.com> Date: Fri, 14 Dec 2018 22:20:19 +0000 Subject: [PATCH 05/57] Add aws-cdk package --- package-lock.json | 1993 +++++++++++++++++++++++++++++++++++++++++++++ package.json | 1 + 2 files changed, 1994 insertions(+) diff --git a/package-lock.json b/package-lock.json index 51dcde4..936a6ca 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4,10 +4,2003 @@ "lockfileVersion": 1, "requires": true, "dependencies": { + "@aws-cdk/applet-js": { + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/@aws-cdk/applet-js/-/applet-js-0.20.0.tgz", + "integrity": "sha512-zOfBnyjf9uJm46VVz0CLuq+sC+UkibWFmi9Ow5qlfBJZ6fizNYvM/bwC7zhPlMYCVvNdulZBLXJ3DK85h6E3XA==", + "requires": { + "@aws-cdk/cdk": "^0.20.0", + "fs-extra": "^7.0.0", + "source-map-support": "^0.5.6", + "yaml": "^1.1.0" + } + }, + "@aws-cdk/cdk": { + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/@aws-cdk/cdk/-/cdk-0.20.0.tgz", + "integrity": "sha512-wzwQIrUUPtHN3Mj5v5w/Eu33Vk/81iM9ilNiBTTdcyw++52OjYQ7iTb47QXeVg8IxGz+lX+lRE/f4IJUNTzRfA==", + "requires": { + "@aws-cdk/cx-api": "^0.20.0", + "js-base64": "^2.4.5", + "json-diff": "^0.3.1" + }, + "dependencies": { + "cli-color": { + "version": "0.1.7", + "resolved": "http://registry.npmjs.org/cli-color/-/cli-color-0.1.7.tgz", + "integrity": "sha1-rcMgD6RxzCEbDaf1ZrcemLnWc0c=", + "requires": { + "es5-ext": "0.8.x" + } + }, + "difflib": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/difflib/-/difflib-0.2.4.tgz", + "integrity": "sha1-teMDYabbAjF21WKJLbhZQKcY9H4=", + "requires": { + "heap": ">= 0.2.0" + } + }, + "dreamopt": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/dreamopt/-/dreamopt-0.6.0.tgz", + "integrity": "sha1-2BPM2sjTnYrVJndVFKE92mZNa0s=", + "requires": { + "wordwrap": ">=0.0.2" + } + }, + "es5-ext": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.8.2.tgz", + "integrity": "sha1-q6jZ4ZQ6iVrJaDemKjmz9V7NlKs=" + }, + "heap": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/heap/-/heap-0.2.6.tgz", + "integrity": "sha1-CH4fELBGky/IWU3Z5tN4r8nR5aw=" + }, + "js-base64": { + "version": "2.4.9", + "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-2.4.9.tgz", + "integrity": "sha512-xcinL3AuDJk7VSzsHgb9DvvIXayBbadtMZ4HFPx8rUszbW1MuNMlwYVC4zzCZ6e1sqZpnNS5ZFYOhXqA39T7LQ==" + }, + "json-diff": { + "version": "0.3.1", + "resolved": "http://registry.npmjs.org/json-diff/-/json-diff-0.3.1.tgz", + "integrity": "sha1-bbw64tJeB1p/1xvNmHRFhmb7aBs=", + "requires": { + "cli-color": "~0.1.6", + "difflib": "~0.2.1", + "dreamopt": "~0.6.0" + } + }, + "wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=" + } + } + }, + "@aws-cdk/cfnspec": { + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/@aws-cdk/cfnspec/-/cfnspec-0.20.0.tgz", + "integrity": "sha512-yU9zxwwogk34BnZb22P6uudsZ6EtJb6YsqFxh4aaC/Uk3lrUh8hlUSLsx2R+F4OFDg1I1hT0m4xjsHVaObe/SQ==", + "requires": { + "md5": "^2.2.1" + } + }, + "@aws-cdk/cloudformation-diff": { + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/@aws-cdk/cloudformation-diff/-/cloudformation-diff-0.20.0.tgz", + "integrity": "sha512-jQN1aT1jLK9MRsmHJnTTZcbqz8Kf/54VhODvHfxcYORrBsGU1dr5XIlwND1eYhILN72YpC3Cdg8n4IpM+qzSkg==", + "requires": { + "@aws-cdk/cfnspec": "^0.20.0", + "@aws-cdk/cx-api": "^0.20.0", + "cli-table": "^0.3.1", + "colors": "^1.2.1", + "fast-deep-equal": "^2.0.1", + "source-map-support": "^0.5.6" + } + }, + "@aws-cdk/cx-api": { + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/@aws-cdk/cx-api/-/cx-api-0.20.0.tgz", + "integrity": "sha512-tbV4msow2+V9xUIDiYzD3ZGBKkdOzMYicR7zXl98i5jJT3EphbMDzulsJ7LVEMKyFIV/1q0NA0k3SJ6T22nVSw==" + }, + "agent-base": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-4.2.1.tgz", + "integrity": "sha512-JVwXMr9nHYTUXsBFKUqhJwvlcYU/blreOEUkhNR2eXZIvwd+c+o5V4MgDPKWnMS/56awN3TRzIP+KoPn+roQtg==", + "requires": { + "es6-promisify": "^5.0.0" + } + }, + "ajv": { + "version": "6.6.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.6.1.tgz", + "integrity": "sha512-ZoJjft5B+EJBjUyu9C9Hc0OZyPZSSlOF+plzouTrg6UlA8f+e/n8NIgBFG/9tppJtpPWfthHakK7juJdNDODww==", + "requires": { + "fast-deep-equal": "^2.0.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=" + }, + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "requires": { + "color-convert": "^1.9.0" + } + }, + "archiver": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/archiver/-/archiver-2.1.1.tgz", + "integrity": "sha1-/2YrSnggFJSj7lRNOjP+dJZQnrw=", + "requires": { + "archiver-utils": "^1.3.0", + "async": "^2.0.0", + "buffer-crc32": "^0.2.1", + "glob": "^7.0.0", + "lodash": "^4.8.0", + "readable-stream": "^2.0.0", + "tar-stream": "^1.5.0", + "zip-stream": "^1.2.0" + } + }, + "archiver-utils": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-1.3.0.tgz", + "integrity": "sha1-5QtMCccL89aA4y/xt5lOn52JUXQ=", + "requires": { + "glob": "^7.0.0", + "graceful-fs": "^4.1.0", + "lazystream": "^1.0.0", + "lodash": "^4.8.0", + "normalize-path": "^2.0.0", + "readable-stream": "^2.0.0" + } + }, + "asn1": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", + "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==", + "requires": { + "safer-buffer": "~2.1.0" + } + }, + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" + }, + "ast-types": { + "version": "0.11.7", + "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.11.7.tgz", + "integrity": "sha512-2mP3TwtkY/aTv5X3ZsMpNAbOnyoC/aMJwJSoaELPkHId0nSQgFcnU4dRW3isxiz7+zBexk0ym3WNVjMiQBnJSw==" + }, + "astral-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-1.0.0.tgz", + "integrity": "sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg==" + }, + "async": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/async/-/async-2.6.1.tgz", + "integrity": "sha512-fNEiL2+AZt6AlAw/29Cr0UDe4sRAHCpEHh54WMz+Bb7QfNcFw4h3loofyJpLeQs4Yx7yuqu/2dLgM5hKOs6HlQ==", + "requires": { + "lodash": "^4.17.10" + } + }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" + }, + "aws-cdk": { + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/aws-cdk/-/aws-cdk-0.20.0.tgz", + "integrity": "sha512-ddrWkiKstafJhRq8P70VXlGXtGNZaxdTs5CfZ64K/dK/2SbftmI+e1dxN801Uwej5oW0VKpoi+h+ooKL0M6abg==", + "requires": { + "@aws-cdk/applet-js": "^0.20.0", + "@aws-cdk/cloudformation-diff": "^0.20.0", + "@aws-cdk/cx-api": "^0.20.0", + "archiver": "^2.1.1", + "aws-sdk": "^2.259.1", + "camelcase": "^5.0.0", + "cli-table": "^0.3.1", + "colors": "^1.2.1", + "decamelize": "^2.0.0", + "fs-extra": "^7.0.0", + "json-diff": "^0.3.1", + "minimatch": ">=3.0", + "promptly": "^0.2.0", + "proxy-agent": "^3.0.1", + "request": "^2.83.0", + "semver": "^5.5.0", + "source-map-support": "^0.5.6", + "table": "^5.1.0", + "yaml": "^1.1.0", + "yargs": "^9.0.1" + } + }, + "aws-sdk": { + "version": "2.376.0", + "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.376.0.tgz", + "integrity": "sha512-7/xsLzgF0GJIFTy2qPmn+dFLQoBpWponm+FS9aHUmjI0UMiTieBJzHUSZ8BHIY/4JukBLc3B5hL5evW+NS4pzw==", + "requires": { + "buffer": "4.9.1", + "events": "1.1.1", + "ieee754": "1.1.8", + "jmespath": "0.15.0", + "querystring": "0.2.0", + "sax": "1.2.1", + "url": "0.10.3", + "uuid": "3.1.0", + "xml2js": "0.4.19" + }, + "dependencies": { + "buffer": { + "version": "4.9.1", + "resolved": "http://registry.npmjs.org/buffer/-/buffer-4.9.1.tgz", + "integrity": "sha1-bRu2AbB6TvztlwlBMgkwJ8lbwpg=", + "requires": { + "base64-js": "^1.0.2", + "ieee754": "^1.1.4", + "isarray": "^1.0.0" + } + }, + "ieee754": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.8.tgz", + "integrity": "sha1-vjPUCsEO8ZJnAfbwii2G+/0a0+Q=" + } + } + }, + "aws-sign2": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", + "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=" + }, + "aws4": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.8.0.tgz", + "integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==" + }, + "balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" + }, + "base64-js": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.0.tgz", + "integrity": "sha512-ccav/yGvoa80BQDljCxsmmQ3Xvx60/UpBIij5QN21W3wBi/hhIC9OoO+KLpu9IJTS9j4DRVJ3aDDF9cMSoa2lw==" + }, + "bcrypt-pbkdf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", + "requires": { + "tweetnacl": "^0.14.3" + } + }, + "bl": { + "version": "1.2.2", + "resolved": "http://registry.npmjs.org/bl/-/bl-1.2.2.tgz", + "integrity": "sha512-e8tQYnZodmebYDWGH7KMRvtzKXaJHx3BbilrgZCfvyLUYdKpK1t5PSPmpkny/SgiTSCnjfLW7v5rlONXVFkQEA==", + "requires": { + "readable-stream": "^2.3.5", + "safe-buffer": "^5.1.1" + } + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.2.1.tgz", + "integrity": "sha512-c+Ko0loDaFfuPWiL02ls9Xd3GO3cPVmUobQ6t3rXNUk304u6hGq+8N/kFi+QEIKhzK3uwolVhLzszmfLmMLnqg==", + "requires": { + "base64-js": "^1.0.2", + "ieee754": "^1.1.4" + } + }, + "buffer-alloc": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/buffer-alloc/-/buffer-alloc-1.2.0.tgz", + "integrity": "sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow==", + "requires": { + "buffer-alloc-unsafe": "^1.1.0", + "buffer-fill": "^1.0.0" + } + }, + "buffer-alloc-unsafe": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz", + "integrity": "sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg==" + }, + "buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI=" + }, + "buffer-fill": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/buffer-fill/-/buffer-fill-1.0.0.tgz", + "integrity": "sha1-+PeLdniYiO858gXNY39o5wISKyw=" + }, + "buffer-from": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", + "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==" + }, + "builtin-modules": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz", + "integrity": "sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8=" + }, + "bytes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", + "integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=" + }, + "camelcase": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.0.0.tgz", + "integrity": "sha512-faqwZqnWxbxn+F1d399ygeamQNy3lPp/H9H6rNrqYh4FSVCtcY+3cub1MxA8o9mDd55mM8Aghuu/kuyYA6VTsA==" + }, + "caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" + }, + "charenc": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz", + "integrity": "sha1-wKHS86cJLgN3S/qD8UwPxXkKhmc=" + }, + "cli-color": { + "version": "0.1.7", + "resolved": "http://registry.npmjs.org/cli-color/-/cli-color-0.1.7.tgz", + "integrity": "sha1-rcMgD6RxzCEbDaf1ZrcemLnWc0c=", + "requires": { + "es5-ext": "0.8.x" + } + }, + "cli-table": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/cli-table/-/cli-table-0.3.1.tgz", + "integrity": "sha1-9TsFJmqLGguTSz0IIebi3FkUriM=", + "requires": { + "colors": "1.0.3" + }, + "dependencies": { + "colors": { + "version": "1.0.3", + "resolved": "http://registry.npmjs.org/colors/-/colors-1.0.3.tgz", + "integrity": "sha1-BDP0TYCWgP3rYO0mDxsMJi6CpAs=" + } + } + }, + "cliui": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-3.2.0.tgz", + "integrity": "sha1-EgYBU3qRbSmUD5NNo7SNWFo5IT0=", + "requires": { + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1", + "wrap-ansi": "^2.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" + }, + "is-fullwidth-code-point": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", + "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", + "requires": { + "number-is-nan": "^1.0.0" + } + }, + "string-width": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", + "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "requires": { + "code-point-at": "^1.0.0", + "is-fullwidth-code-point": "^1.0.0", + "strip-ansi": "^3.0.0" + } + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "http://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "requires": { + "ansi-regex": "^2.0.0" + } + } + } + }, + "co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=" + }, + "code-point-at": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", + "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=" + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" + }, + "colors": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.3.3.tgz", + "integrity": "sha512-mmGt/1pZqYRjMxB1axhTo16/snVZ5krrKkcmMeVKxzECMMXoCgnvTPp10QgHfcbQZw8Dq2jMNG6je4JlWU0gWg==" + }, + "combined-stream": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.7.tgz", + "integrity": "sha512-brWl9y6vOB1xYPZcpZde3N9zDByXTosAeMDo4p1wzo6UMOX4vumB+TP1RZ76sfE6Md68Q0NJSrE/gbezd4Ul+w==", + "requires": { + "delayed-stream": "~1.0.0" + } + }, + "compress-commons": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-1.2.2.tgz", + "integrity": "sha1-UkqfEJA/OoEzibAiXSfEi7dRiQ8=", + "requires": { + "buffer-crc32": "^0.2.1", + "crc32-stream": "^2.0.0", + "normalize-path": "^2.0.0", + "readable-stream": "^2.0.0" + } + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" + }, + "core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" + }, + "crc": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/crc/-/crc-3.8.0.tgz", + "integrity": "sha512-iX3mfgcTMIq3ZKLIsVFAbv7+Mc10kxabAGQb8HvjA1o3T1PIYprbakQ65d3I+2HGHt6nSKkM9PYjgoJO2KcFBQ==", + "requires": { + "buffer": "^5.1.0" + } + }, + "crc32-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-2.0.0.tgz", + "integrity": "sha1-483TtN8xaN10494/u8t7KX/pCPQ=", + "requires": { + "crc": "^3.4.4", + "readable-stream": "^2.0.0" + } + }, + "cross-spawn": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz", + "integrity": "sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk=", + "requires": { + "lru-cache": "^4.0.1", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + } + }, + "crypt": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz", + "integrity": "sha1-iNf/fsDfuG9xPch7u0LQRNPmxBs=" + }, + "dashdash": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", + "requires": { + "assert-plus": "^1.0.0" + } + }, + "data-uri-to-buffer": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-1.2.0.tgz", + "integrity": "sha512-vKQ9DTQPN1FLYiiEEOQ6IBGFqvjCa5rSK3cWMy/Nespm5d/x3dGFT9UBZnkLxCwua/IXBi2TYnwTEpsOvhC4UQ==" + }, + "debug": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", + "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", + "requires": { + "ms": "^2.1.1" + } + }, + "decamelize": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-2.0.0.tgz", + "integrity": "sha512-Ikpp5scV3MSYxY39ymh45ZLEecsTdv/Xj2CaQfI8RLMuwi7XvjX9H/fhraiSuU+C5w5NTDu4ZU72xNiZnurBPg==", + "requires": { + "xregexp": "4.0.0" + } + }, + "deep-is": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", + "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=" + }, + "degenerator": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-1.0.4.tgz", + "integrity": "sha1-/PSQo37OJmRk2cxDGrmMWBnO0JU=", + "requires": { + "ast-types": "0.x.x", + "escodegen": "1.x.x", + "esprima": "3.x.x" + } + }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" + }, + "depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=" + }, + "difflib": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/difflib/-/difflib-0.2.4.tgz", + "integrity": "sha1-teMDYabbAjF21WKJLbhZQKcY9H4=", + "requires": { + "heap": ">= 0.2.0" + } + }, + "dreamopt": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/dreamopt/-/dreamopt-0.6.0.tgz", + "integrity": "sha1-2BPM2sjTnYrVJndVFKE92mZNa0s=", + "requires": { + "wordwrap": ">=0.0.2" + } + }, + "ecc-jsbn": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", + "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", + "requires": { + "jsbn": "~0.1.0", + "safer-buffer": "^2.1.0" + } + }, + "end-of-stream": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.1.tgz", + "integrity": "sha512-1MkrZNvWTKCaigbn+W15elq2BB/L22nqrSY5DKlo3X6+vclJm8Bb5djXJBmEX6fS3+zCh/F4VBK5Z2KxJt4s2Q==", + "requires": { + "once": "^1.4.0" + } + }, + "error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "requires": { + "is-arrayish": "^0.2.1" + } + }, + "es5-ext": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.8.2.tgz", + "integrity": "sha1-q6jZ4ZQ6iVrJaDemKjmz9V7NlKs=" + }, + "es6-promise": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.5.tgz", + "integrity": "sha512-n6wvpdE43VFtJq+lUDYDBFUwV8TZbuGXLV4D6wKafg13ldznKsyEvatubnmUe31zcvelSzOHF+XbaT+Bl9ObDg==" + }, + "es6-promisify": { + "version": "5.0.0", + "resolved": "http://registry.npmjs.org/es6-promisify/-/es6-promisify-5.0.0.tgz", + "integrity": "sha1-UQnWLz5W6pZ8S2NQWu8IKRyKUgM=", + "requires": { + "es6-promise": "^4.0.3" + } + }, + "escodegen": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.11.0.tgz", + "integrity": "sha512-IeMV45ReixHS53K/OmfKAIztN/igDHzTJUhZM3k1jMhIZWjk45SMwAtBsEXiJp3vSPmTcu6CXn7mDvFHRN66fw==", + "requires": { + "esprima": "^3.1.3", + "estraverse": "^4.2.0", + "esutils": "^2.0.2", + "optionator": "^0.8.1", + "source-map": "~0.6.1" + } + }, + "esprima": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-3.1.3.tgz", + "integrity": "sha1-/cpRzuYTOJXjyI1TXOSdv/YqRjM=" + }, + "estraverse": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.2.0.tgz", + "integrity": "sha1-De4/7TH81GlhjOc0IJn8GvoL2xM=" + }, + "esutils": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz", + "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=" + }, + "events": { + "version": "1.1.1", + "resolved": "http://registry.npmjs.org/events/-/events-1.1.1.tgz", + "integrity": "sha1-nr23Y1rQmccNzEwqH1AEKI6L2SQ=" + }, + "execa": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-0.7.0.tgz", + "integrity": "sha1-lEvs00zEHuMqY6n68nrVpl/Fl3c=", + "requires": { + "cross-spawn": "^5.0.1", + "get-stream": "^3.0.0", + "is-stream": "^1.1.0", + "npm-run-path": "^2.0.0", + "p-finally": "^1.0.0", + "signal-exit": "^3.0.0", + "strip-eof": "^1.0.0" + } + }, + "extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" + }, + "extsprintf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=" + }, + "fast-deep-equal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", + "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=" + }, + "fast-json-stable-stringify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", + "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=" + }, + "fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=" + }, + "file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==" + }, + "find-up": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", + "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", + "requires": { + "locate-path": "^2.0.0" + } + }, + "forever-agent": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=" + }, + "form-data": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", + "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + } + }, + "fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==" + }, + "fs-extra": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", + "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", + "requires": { + "graceful-fs": "^4.1.2", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + } + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" + }, + "ftp": { + "version": "0.3.10", + "resolved": "https://registry.npmjs.org/ftp/-/ftp-0.3.10.tgz", + "integrity": "sha1-kZfYYa2BQvPmPVqDv+TFn3MwiF0=", + "requires": { + "readable-stream": "1.1.x", + "xregexp": "2.0.0" + }, + "dependencies": { + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" + }, + "readable-stream": { + "version": "1.1.14", + "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", + "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "0.0.1", + "string_decoder": "~0.10.x" + } + }, + "string_decoder": { + "version": "0.10.31", + "resolved": "http://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" + }, + "xregexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/xregexp/-/xregexp-2.0.0.tgz", + "integrity": "sha1-UqY+VsoLhKfzpfPWGHLxJq16WUM=" + } + } + }, + "get-caller-file": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.3.tgz", + "integrity": "sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w==" + }, + "get-stream": { + "version": "3.0.0", + "resolved": "http://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", + "integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=" + }, + "get-uri": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-2.0.2.tgz", + "integrity": "sha512-ZD325dMZOgerGqF/rF6vZXyFGTAay62svjQIT+X/oU2PtxYpFxvSkbsdi+oxIrsNxlZVd4y8wUDqkaExWTI/Cw==", + "requires": { + "data-uri-to-buffer": "1", + "debug": "2", + "extend": "3", + "file-uri-to-path": "1", + "ftp": "~0.3.10", + "readable-stream": "2" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + } + } + }, + "getpass": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", + "requires": { + "assert-plus": "^1.0.0" + } + }, + "glob": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz", + "integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==", + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "graceful-fs": { + "version": "4.1.15", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.15.tgz", + "integrity": "sha512-6uHUhOPEBgQ24HM+r6b/QwWfZq+yiFcipKFrOFiBEnWdy5sdzYoi+pJeQaPI5qOLRFqWmAXUPQNsielzdLoecA==" + }, + "har-schema": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", + "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=" + }, + "har-validator": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.3.tgz", + "integrity": "sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==", + "requires": { + "ajv": "^6.5.5", + "har-schema": "^2.0.0" + } + }, + "heap": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/heap/-/heap-0.2.6.tgz", + "integrity": "sha1-CH4fELBGky/IWU3Z5tN4r8nR5aw=" + }, + "hosted-git-info": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.7.1.tgz", + "integrity": "sha512-7T/BxH19zbcCTa8XkMlbK5lTo1WtgkFi3GvdWEyNuc4Vex7/9Dqbnpsf4JMydcfj9HCg4zUWFTL3Za6lapg5/w==" + }, + "http-errors": { + "version": "1.6.3", + "resolved": "http://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", + "integrity": "sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0=", + "requires": { + "depd": "~1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.0", + "statuses": ">= 1.4.0 < 2" + } + }, + "http-proxy-agent": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-2.1.0.tgz", + "integrity": "sha512-qwHbBLV7WviBl0rQsOzH6o5lwyOIvwp/BdFnvVxXORldu5TmjFfjzBcWUWS5kWAZhmv+JtiDhSuQCp4sBfbIgg==", + "requires": { + "agent-base": "4", + "debug": "3.1.0" + }, + "dependencies": { + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + } + } + }, + "http-signature": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", + "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", + "requires": { + "assert-plus": "^1.0.0", + "jsprim": "^1.2.2", + "sshpk": "^1.7.0" + } + }, + "https-proxy-agent": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-2.2.1.tgz", + "integrity": "sha512-HPCTS1LW51bcyMYbxUIOO4HEOlQ1/1qRaFWcyxvwaqUS9TY88aoEuHUY33kuAh1YhVVaDQhLZsnPd+XNARWZlQ==", + "requires": { + "agent-base": "^4.1.0", + "debug": "^3.1.0" + } + }, + "iconv-lite": { + "version": "0.4.23", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.23.tgz", + "integrity": "sha512-neyTUVFtahjf0mB3dZT77u+8O0QB89jFdnBkd5P1JgYPbPaia3gXXOVL2fq8VyU2gMMD7SaN7QukTB/pmXYvDA==", + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + }, + "ieee754": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.12.tgz", + "integrity": "sha512-GguP+DRY+pJ3soyIiGPTvdiVXjZ+DbXOxGpXn3eMvNW4x4irjqXm4wHKscC+TfxSJ0yw/S1F24tqdMNsMZTiLA==" + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" + }, + "invert-kv": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-1.0.0.tgz", + "integrity": "sha1-EEqOSqym09jNFXqO+L+rLXo//bY=" + }, + "ip": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.5.tgz", + "integrity": "sha1-vd7XARQpCCjAoDnnLvJfWq7ENUo=" + }, + "is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=" + }, + "is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==" + }, + "is-builtin-module": { + "version": "1.0.0", + "resolved": "http://registry.npmjs.org/is-builtin-module/-/is-builtin-module-1.0.0.tgz", + "integrity": "sha1-VAVy0096wxGfj3bDDLwbHgN6/74=", + "requires": { + "builtin-modules": "^1.0.0" + } + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=" + }, + "is-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=" + }, + "is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=" + }, + "isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" + }, + "jmespath": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/jmespath/-/jmespath-0.15.0.tgz", + "integrity": "sha1-o/Iiqarp+Wb10nx5ZRDigJF2Qhc=" + }, + "jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=" + }, + "json-diff": { + "version": "0.3.1", + "resolved": "http://registry.npmjs.org/json-diff/-/json-diff-0.3.1.tgz", + "integrity": "sha1-bbw64tJeB1p/1xvNmHRFhmb7aBs=", + "requires": { + "cli-color": "~0.1.6", + "difflib": "~0.2.1", + "dreamopt": "~0.6.0" + } + }, + "json-schema": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", + "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=" + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" + }, + "json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" + }, + "jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=", + "requires": { + "graceful-fs": "^4.1.6" + } + }, + "jsprim": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", + "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", + "requires": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.2.3", + "verror": "1.10.0" + } + }, + "lazystream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.0.tgz", + "integrity": "sha1-9plf4PggOS9hOWvolGJAe7dxaOQ=", + "requires": { + "readable-stream": "^2.0.5" + } + }, + "lcid": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/lcid/-/lcid-1.0.0.tgz", + "integrity": "sha1-MIrMr6C8SDo4Z7S28rlQYlHRuDU=", + "requires": { + "invert-kv": "^1.0.0" + } + }, + "levn": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", + "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", + "requires": { + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2" + } + }, + "load-json-file": { + "version": "2.0.0", + "resolved": "http://registry.npmjs.org/load-json-file/-/load-json-file-2.0.0.tgz", + "integrity": "sha1-eUfkIUmvgNaWy/eXvKq8/h/inKg=", + "requires": { + "graceful-fs": "^4.1.2", + "parse-json": "^2.2.0", + "pify": "^2.0.0", + "strip-bom": "^3.0.0" + } + }, + "locate-path": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", + "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=", + "requires": { + "p-locate": "^2.0.0", + "path-exists": "^3.0.0" + } + }, + "lodash": { + "version": "4.17.11", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz", + "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==" + }, + "lru-cache": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz", + "integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==", + "requires": { + "pseudomap": "^1.0.2", + "yallist": "^2.1.2" + } + }, + "md5": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/md5/-/md5-2.2.1.tgz", + "integrity": "sha1-U6s41f48iJG6RlMp6iP6wFQBJvk=", + "requires": { + "charenc": "~0.0.1", + "crypt": "~0.0.1", + "is-buffer": "~1.1.1" + } + }, + "mem": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/mem/-/mem-1.1.0.tgz", + "integrity": "sha1-Xt1StIXKHZAP5kiVUFOZoN+kX3Y=", + "requires": { + "mimic-fn": "^1.0.0" + } + }, + "mime-db": { + "version": "1.37.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.37.0.tgz", + "integrity": "sha512-R3C4db6bgQhlIhPU48fUtdVmKnflq+hRdad7IyKhtFj06VPNVdk2RhiYL3UjQIlso8L+YxAtFkobT0VK+S/ybg==" + }, + "mime-types": { + "version": "2.1.21", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.21.tgz", + "integrity": "sha512-3iL6DbwpyLzjR3xHSFNFeb9Nz/M8WDkX33t1GFQnFOllWk8pOrh/LSrB5OXlnlW5P9LH73X6loW/eogc+F5lJg==", + "requires": { + "mime-db": "~1.37.0" + } + }, + "mimic-fn": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz", + "integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==" + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "ms": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", + "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==" + }, + "mute-stream": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.7.tgz", + "integrity": "sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s=" + }, + "netmask": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/netmask/-/netmask-1.0.6.tgz", + "integrity": "sha1-ICl+idhvb2QA8lDZ9Pa0wZRfzTU=" + }, + "normalize-package-data": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.4.0.tgz", + "integrity": "sha512-9jjUFbTPfEy3R/ad/2oNbKtW9Hgovl5O1FvFWKkKblNXoN/Oou6+9+KKohPK13Yc3/TyunyWhJp6gvRNR/PPAw==", + "requires": { + "hosted-git-info": "^2.1.4", + "is-builtin-module": "^1.0.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" + } + }, + "normalize-path": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", + "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", + "requires": { + "remove-trailing-separator": "^1.0.1" + } + }, + "npm-run-path": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", + "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=", + "requires": { + "path-key": "^2.0.0" + } + }, + "number-is-nan": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", + "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=" + }, + "oauth-sign": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", + "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==" + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "requires": { + "wrappy": "1" + } + }, + "optionator": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.2.tgz", + "integrity": "sha1-NkxeQJ0/TWMB1sC0wFu6UBgK62Q=", + "requires": { + "deep-is": "~0.1.3", + "fast-levenshtein": "~2.0.4", + "levn": "~0.3.0", + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2", + "wordwrap": "~1.0.0" + } + }, + "os-locale": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-2.1.0.tgz", + "integrity": "sha512-3sslG3zJbEYcaC4YVAvDorjGxc7tv6KVATnLPZONiljsUncvihe9BQoVCEs0RZ1kmf4Hk9OBqlZfJZWI4GanKA==", + "requires": { + "execa": "^0.7.0", + "lcid": "^1.0.0", + "mem": "^1.1.0" + } + }, + "p-finally": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", + "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=" + }, + "p-limit": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", + "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", + "requires": { + "p-try": "^1.0.0" + } + }, + "p-locate": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", + "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=", + "requires": { + "p-limit": "^1.1.0" + } + }, + "p-try": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", + "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=" + }, + "pac-proxy-agent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-3.0.0.tgz", + "integrity": "sha512-AOUX9jES/EkQX2zRz0AW7lSx9jD//hQS8wFXBvcnd/J2Py9KaMJMqV/LPqJssj1tgGufotb2mmopGPR15ODv1Q==", + "requires": { + "agent-base": "^4.2.0", + "debug": "^3.1.0", + "get-uri": "^2.0.0", + "http-proxy-agent": "^2.1.0", + "https-proxy-agent": "^2.2.1", + "pac-resolver": "^3.0.0", + "raw-body": "^2.2.0", + "socks-proxy-agent": "^4.0.1" + } + }, + "pac-resolver": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-3.0.0.tgz", + "integrity": "sha512-tcc38bsjuE3XZ5+4vP96OfhOugrX+JcnpUbhfuc4LuXBLQhoTthOstZeoQJBDnQUDYzYmdImKsbz0xSl1/9qeA==", + "requires": { + "co": "^4.6.0", + "degenerator": "^1.0.4", + "ip": "^1.1.5", + "netmask": "^1.0.6", + "thunkify": "^2.1.2" + } + }, + "parse-json": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz", + "integrity": "sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=", + "requires": { + "error-ex": "^1.2.0" + } + }, + "path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=" + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "http://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" + }, + "path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=" + }, + "path-type": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-2.0.0.tgz", + "integrity": "sha1-8BLMuEFbcJb8LaoQVMPXI4lZTHM=", + "requires": { + "pify": "^2.0.0" + } + }, + "performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" + }, + "pify": { + "version": "2.3.0", + "resolved": "http://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=" + }, + "prelude-ls": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", + "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=" + }, + "process-nextick-args": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", + "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==" + }, + "promptly": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/promptly/-/promptly-0.2.1.tgz", + "integrity": "sha1-ZETnyk29mJnn7rXsOSKCfr3CKzs=", + "requires": { + "read": "~1.0.4" + } + }, + "proxy-agent": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-3.0.3.tgz", + "integrity": "sha512-PXVVVuH9tiQuxQltFJVSnXWuDtNr+8aNBP6XVDDCDiUuDN8eRCm+ii4/mFWmXWEA0w8jjJSlePa4LXlM4jIzNA==", + "requires": { + "agent-base": "^4.2.0", + "debug": "^3.1.0", + "http-proxy-agent": "^2.1.0", + "https-proxy-agent": "^2.2.1", + "lru-cache": "^4.1.2", + "pac-proxy-agent": "^3.0.0", + "proxy-from-env": "^1.0.0", + "socks-proxy-agent": "^4.0.1" + } + }, + "proxy-from-env": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.0.0.tgz", + "integrity": "sha1-M8UDmPcOp+uW0h97gXYwpVeRx+4=" + }, + "pseudomap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", + "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=" + }, + "psl": { + "version": "1.1.31", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.1.31.tgz", + "integrity": "sha512-/6pt4+C+T+wZUieKR620OpzN/LlnNKuWjy1iFLQ/UG35JqHlR/89MP1d96dUfkf6Dne3TuLQzOYEYshJ+Hx8mw==" + }, + "punycode": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", + "integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=" + }, + "qs": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", + "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==" + }, + "querystring": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", + "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=" + }, + "raw-body": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.3.3.tgz", + "integrity": "sha512-9esiElv1BrZoI3rCDuOuKCBRbuApGGaDPQfjSflGxdy4oyzqghxu6klEkkVIvBje+FF0BX9coEv8KqW6X/7njw==", + "requires": { + "bytes": "3.0.0", + "http-errors": "1.6.3", + "iconv-lite": "0.4.23", + "unpipe": "1.0.0" + } + }, + "read": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/read/-/read-1.0.7.tgz", + "integrity": "sha1-s9oZvQUkMal2cdRKQmNK33ELQMQ=", + "requires": { + "mute-stream": "~0.0.4" + } + }, + "read-pkg": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-2.0.0.tgz", + "integrity": "sha1-jvHAYjxqbbDcZxPEv6xGMysjaPg=", + "requires": { + "load-json-file": "^2.0.0", + "normalize-package-data": "^2.3.2", + "path-type": "^2.0.0" + } + }, + "read-pkg-up": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-2.0.0.tgz", + "integrity": "sha1-a3KoBImE4MQeeVEP1en6mbO1Sb4=", + "requires": { + "find-up": "^2.0.0", + "read-pkg": "^2.0.0" + } + }, + "readable-stream": { + "version": "2.3.6", + "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "remove-trailing-separator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", + "integrity": "sha1-wkvOKig62tW8P1jg1IJJuSN52O8=" + }, + "request": { + "version": "2.88.0", + "resolved": "https://registry.npmjs.org/request/-/request-2.88.0.tgz", + "integrity": "sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg==", + "requires": { + "aws-sign2": "~0.7.0", + "aws4": "^1.8.0", + "caseless": "~0.12.0", + "combined-stream": "~1.0.6", + "extend": "~3.0.2", + "forever-agent": "~0.6.1", + "form-data": "~2.3.2", + "har-validator": "~5.1.0", + "http-signature": "~1.2.0", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.19", + "oauth-sign": "~0.9.0", + "performance-now": "^2.1.0", + "qs": "~6.5.2", + "safe-buffer": "^5.1.2", + "tough-cookie": "~2.4.3", + "tunnel-agent": "^0.6.0", + "uuid": "^3.3.2" + }, + "dependencies": { + "uuid": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", + "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==" + } + } + }, + "require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=" + }, + "require-main-filename": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-1.0.1.tgz", + "integrity": "sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE=" + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "sax": { + "version": "1.2.1", + "resolved": "http://registry.npmjs.org/sax/-/sax-1.2.1.tgz", + "integrity": "sha1-e45lYZCyKOgaZq6nSEgNgozS03o=" + }, + "semver": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.6.0.tgz", + "integrity": "sha512-RS9R6R35NYgQn++fkDWaOmqGoj4Ek9gGs+DPxNUZKuwE183xjJroKvyo1IzVFeXvUrvmALy6FWD5xrdJT25gMg==" + }, + "set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=" + }, + "setprototypeof": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", + "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==" + }, + "shebang-command": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", + "requires": { + "shebang-regex": "^1.0.0" + } + }, + "shebang-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=" + }, + "signal-exit": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", + "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=" + }, + "slice-ansi": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-2.0.0.tgz", + "integrity": "sha512-4j2WTWjp3GsZ+AOagyzVbzp4vWGtZ0hEZ/gDY/uTvm6MTxUfTUIsnMIFb1bn8o0RuXiqUw15H1bue8f22Vw2oQ==", + "requires": { + "ansi-styles": "^3.2.0", + "astral-regex": "^1.0.0", + "is-fullwidth-code-point": "^2.0.0" + } + }, + "smart-buffer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.0.1.tgz", + "integrity": "sha512-RFqinRVJVcCAL9Uh1oVqE6FZkqsyLiVOYEZ20TqIOjuX7iFVJ+zsbs4RIghnw/pTs7mZvt8ZHhvm1ZUrR4fykg==" + }, + "socks": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.2.2.tgz", + "integrity": "sha512-g6wjBnnMOZpE0ym6e0uHSddz9p3a+WsBaaYQaBaSCJYvrC4IXykQR9MNGjLQf38e9iIIhp3b1/Zk8YZI3KGJ0Q==", + "requires": { + "ip": "^1.1.5", + "smart-buffer": "^4.0.1" + } + }, + "socks-proxy-agent": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-4.0.1.tgz", + "integrity": "sha512-Kezx6/VBguXOsEe5oU3lXYyKMi4+gva72TwJ7pQY5JfqUx2nMk7NXA6z/mpNqIlfQjWYVfeuNvQjexiTaTn6Nw==", + "requires": { + "agent-base": "~4.2.0", + "socks": "~2.2.0" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" + }, + "source-map-support": { + "version": "0.5.9", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.9.tgz", + "integrity": "sha512-gR6Rw4MvUlYy83vP0vxoVNzM6t8MUXqNuRsuBmBHQDu1Fh6X015FrLdgoDKcNdkwGubozq0P4N0Q37UyFVr1EA==", + "requires": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "spdx-correct": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.0.tgz", + "integrity": "sha512-lr2EZCctC2BNR7j7WzJ2FpDznxky1sjfxvvYEyzxNyb6lZXHODmEoJeFu4JupYlkfha1KZpJyoqiJ7pgA1qq8Q==", + "requires": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "spdx-exceptions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.2.0.tgz", + "integrity": "sha512-2XQACfElKi9SlVb1CYadKDXvoajPgBVPn/gOQLrTvHdElaVhr7ZEbqJaRnJLVNeaI4cMEAgVCeBMKF6MWRDCRA==" + }, + "spdx-expression-parse": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.0.tgz", + "integrity": "sha512-Yg6D3XpRD4kkOmTpdgbUiEJFKghJH03fiC1OPll5h/0sO6neh2jqRDVHOQ4o/LMea0tgCkbMgea5ip/e+MkWyg==", + "requires": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "spdx-license-ids": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.2.tgz", + "integrity": "sha512-qky9CVt0lVIECkEsYbNILVnPvycuEBkXoMFLRWsREkomQLevYhtRKC+R91a5TOAQ3bCMjikRwhyaRqj1VYatYg==" + }, + "sshpk": { + "version": "1.15.2", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.15.2.tgz", + "integrity": "sha512-Ra/OXQtuh0/enyl4ETZAfTaeksa6BXks5ZcjpSUNrjBr0DvrJKX+1fsKDPpT9TBXgHAFsa4510aNVgI8g/+SzA==", + "requires": { + "asn1": "~0.2.3", + "assert-plus": "^1.0.0", + "bcrypt-pbkdf": "^1.0.0", + "dashdash": "^1.12.0", + "ecc-jsbn": "~0.1.1", + "getpass": "^0.1.1", + "jsbn": "~0.1.0", + "safer-buffer": "^2.0.2", + "tweetnacl": "~0.14.0" + } + }, + "statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=" + }, + "string-width": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "requires": { + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^4.0.0" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "http://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "requires": { + "safe-buffer": "~5.1.0" + } + }, + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "requires": { + "ansi-regex": "^3.0.0" + } + }, + "strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=" + }, + "strip-eof": { + "version": "1.0.0", + "resolved": "http://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", + "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=" + }, + "table": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/table/-/table-5.1.1.tgz", + "integrity": "sha512-NUjapYb/qd4PeFW03HnAuOJ7OMcBkJlqeClWxeNlQ0lXGSb52oZXGzkO0/I0ARegQ2eUT1g2VDJH0eUxDRcHmw==", + "requires": { + "ajv": "^6.6.1", + "lodash": "^4.17.11", + "slice-ansi": "2.0.0", + "string-width": "^2.1.1" + } + }, + "tar-stream": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-1.6.2.tgz", + "integrity": "sha512-rzS0heiNf8Xn7/mpdSVVSMAWAoy9bfb1WOTYC78Z0UQKeKa/CWS8FOq0lKGNa8DWKAn9gxjCvMLYc5PGXYlK2A==", + "requires": { + "bl": "^1.0.0", + "buffer-alloc": "^1.2.0", + "end-of-stream": "^1.0.0", + "fs-constants": "^1.0.0", + "readable-stream": "^2.3.0", + "to-buffer": "^1.1.1", + "xtend": "^4.0.0" + } + }, + "thunkify": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/thunkify/-/thunkify-2.1.2.tgz", + "integrity": "sha1-+qDp0jDFGsyVyhOjYawFyn4EVT0=" + }, + "to-buffer": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.1.1.tgz", + "integrity": "sha512-lx9B5iv7msuFYE3dytT+KE5tap+rNYw+K4jVkb9R/asAb+pbBSM17jtunHplhBe6RRJdZx3Pn2Jph24O32mOVg==" + }, + "tough-cookie": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz", + "integrity": "sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ==", + "requires": { + "psl": "^1.1.24", + "punycode": "^1.4.1" + }, + "dependencies": { + "punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=" + } + } + }, + "tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", + "requires": { + "safe-buffer": "^5.0.1" + } + }, + "tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=" + }, + "type-check": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", + "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", + "requires": { + "prelude-ls": "~1.1.2" + } + }, "typescript": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.2.2.tgz", "integrity": "sha512-VCj5UiSyHBjwfYacmDuc/NOk4QQixbE+Wn7MFJuS0nRuPQbof132Pw4u53dm264O8LPc2MVsc7RJNml5szurkg==" + }, + "universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==" + }, + "unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" + }, + "uri-js": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", + "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==", + "requires": { + "punycode": "^2.1.0" + }, + "dependencies": { + "punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" + } + } + }, + "url": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/url/-/url-0.10.3.tgz", + "integrity": "sha1-Ah5NnHcF8hu/N9A861h2dAJ3TGQ=", + "requires": { + "punycode": "1.3.2", + "querystring": "0.2.0" + } + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" + }, + "uuid": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.1.0.tgz", + "integrity": "sha512-DIWtzUkw04M4k3bf1IcpS2tngXEL26YUD2M0tMDUpnUrz2hgzUBlD55a4FjdLGPvfHxS6uluGWvaVEqgBcVa+g==" + }, + "validate-npm-package-license": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "requires": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, + "verror": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", + "requires": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + } + }, + "which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "requires": { + "isexe": "^2.0.0" + } + }, + "which-module": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", + "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=" + }, + "wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=" + }, + "wrap-ansi": { + "version": "2.1.0", + "resolved": "http://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz", + "integrity": "sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=", + "requires": { + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1" + }, + "dependencies": { + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" + }, + "is-fullwidth-code-point": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", + "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", + "requires": { + "number-is-nan": "^1.0.0" + } + }, + "string-width": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", + "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "requires": { + "code-point-at": "^1.0.0", + "is-fullwidth-code-point": "^1.0.0", + "strip-ansi": "^3.0.0" + } + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "http://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "requires": { + "ansi-regex": "^2.0.0" + } + } + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" + }, + "xml2js": { + "version": "0.4.19", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.19.tgz", + "integrity": "sha512-esZnJZJOiJR9wWKMyuvSE1y6Dq5LCuJanqhxslH2bxM6duahNZ+HMpCLhBQGZkbX6xRf8x1Y2eJlgt2q3qo49Q==", + "requires": { + "sax": ">=0.6.0", + "xmlbuilder": "~9.0.1" + } + }, + "xmlbuilder": { + "version": "9.0.7", + "resolved": "http://registry.npmjs.org/xmlbuilder/-/xmlbuilder-9.0.7.tgz", + "integrity": "sha1-Ey7mPS7FVlxVfiD0wi35rKaGsQ0=" + }, + "xregexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/xregexp/-/xregexp-4.0.0.tgz", + "integrity": "sha512-PHyM+sQouu7xspQQwELlGwwd05mXUFqwFYfqPO0cC7x4fxyHnnuetmQr6CjJiafIDoH4MogHb9dOoJzR/Y4rFg==" + }, + "xtend": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz", + "integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68=" + }, + "y18n": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-3.2.1.tgz", + "integrity": "sha1-bRX7qITAhnnA136I53WegR4H+kE=" + }, + "yallist": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", + "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=" + }, + "yaml": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.1.0.tgz", + "integrity": "sha512-MD5Ptelnnjfj/w4UqdNguD9Ipzm3ws6bNiYkGkl4lkfGMU1V7QYyHkRh28EiHPdprSvV+xAhvLJ6ifyALYw7wA==" + }, + "yargs": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-9.0.1.tgz", + "integrity": "sha1-UqzCP+7Kw0BCB47njAwAf1CF20w=", + "requires": { + "camelcase": "^4.1.0", + "cliui": "^3.2.0", + "decamelize": "^1.1.1", + "get-caller-file": "^1.0.1", + "os-locale": "^2.0.0", + "read-pkg-up": "^2.0.0", + "require-directory": "^2.1.1", + "require-main-filename": "^1.0.1", + "set-blocking": "^2.0.0", + "string-width": "^2.0.0", + "which-module": "^2.0.0", + "y18n": "^3.2.1", + "yargs-parser": "^7.0.0" + }, + "dependencies": { + "camelcase": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz", + "integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=" + }, + "decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=" + } + } + }, + "yargs-parser": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-7.0.0.tgz", + "integrity": "sha1-jQrELxbqVd69MyyvTEA4s+P139k=", + "requires": { + "camelcase": "^4.1.0" + }, + "dependencies": { + "camelcase": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz", + "integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=" + } + } + }, + "zip-stream": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-1.2.0.tgz", + "integrity": "sha1-qLxF9MG0lpnGuQGYuqyqzbzUugQ=", + "requires": { + "archiver-utils": "^1.3.0", + "compress-commons": "^1.2.0", + "lodash": "^4.8.0", + "readable-stream": "^2.0.0" + } } } } diff --git a/package.json b/package.json index c3a2c93..86d1bd9 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ "author": "Go Free Range Ltd", "license": "ISC", "dependencies": { + "aws-cdk": "^0.20.0", "typescript": "^3.2.2" } } From 53a7f9c6086abd1c0b5a0bca789eee918eb5e1c2 Mon Sep 17 00:00:00 2001 From: James Mead <3169+floehopper@users.noreply.github.com> Date: Fri, 14 Dec 2018 22:09:31 +0000 Subject: [PATCH 06/57] Define empty aws-cdk stack & app --- index.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/index.ts b/index.ts index b672f02..5d8f770 100644 --- a/index.ts +++ b/index.ts @@ -1,4 +1,8 @@ -class TrelloBackupStack { +import cdk = require('@aws-cdk/cdk'); + +class TrelloBackupStack extends cdk.Stack { } -new TrelloBackupStack(); +const app = new cdk.App(); +new TrelloBackupStack(app, 'TrelloBackup'); +app.run(); From eb618650b5ac3007302d4ec4997375f2af9549d9 Mon Sep 17 00:00:00 2001 From: James Mead <3169+floehopper@users.noreply.github.com> Date: Fri, 14 Dec 2018 22:49:58 +0000 Subject: [PATCH 07/57] Add cdk.json & document how cdk commands can be executed --- README.md | 28 ++++++++++++++++++++++++++++ cdk.json | 3 +++ 2 files changed, 31 insertions(+) create mode 100644 cdk.json diff --git a/README.md b/README.md index 356a734..bdb8cc3 100644 --- a/README.md +++ b/README.md @@ -5,8 +5,36 @@ ## Setup +### Install node packages + npm ci +### Configure AWS + +* You need to specify the AWS credentials and default region to use when running +the `cdk` command-line tool in the same way you would configure the `aws` +command-line tool. See [Configuring the AWS CDK Toolkit][1] for details. + +* I recommend using `aws configure` and/or `~/.aws/credentials` & `~/.aws/config` +to setup a named profile and then use the `--profile ` option +with the `cdk` command-line tool to select the relevant AWS profile. + ## Build npm run build + +## Execute CDK commands + + cdk --profile + +* See [Command-line Toolkit][2] for a list of commands and options to use with +the `cdk` command-line tool. + +* The `cdk.json` file defines an appropriate value for the `--app` option, so +there is no need to specify this unless you want to override that value. + +* `node_modules/.bin` must be in your `PATH` environment variable in order to +be able to run the `cdk` command-line tool. + +[1]: https://awslabs.github.io/aws-cdk/getting-started.html#configuring-the-cdk-toolkit +[2]: https://awslabs.github.io/aws-cdk/tools.html#command-line-toolkit-cdk diff --git a/cdk.json b/cdk.json new file mode 100644 index 0000000..443ab0e --- /dev/null +++ b/cdk.json @@ -0,0 +1,3 @@ +{ + "app": "node index.js" +} From 68540aad053136ec4e400c0b674f3b7422b727b4 Mon Sep 17 00:00:00 2001 From: James Mead <3169+floehopper@users.noreply.github.com> Date: Sat, 15 Dec 2018 00:17:27 +0000 Subject: [PATCH 08/57] Add default constructor to stack class This is where I'll add custom code to define the AWS resources which make up the stack. --- index.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/index.ts b/index.ts index 5d8f770..a52410e 100644 --- a/index.ts +++ b/index.ts @@ -1,6 +1,9 @@ import cdk = require('@aws-cdk/cdk'); class TrelloBackupStack extends cdk.Stack { + constructor(parent: cdk.App, id: string, props?: cdk.StackProps) { + super(parent, id, props); + } } const app = new cdk.App(); From a7b25e050b47c08f00136ac14858bac99020ef1e Mon Sep 17 00:00:00 2001 From: James Mead <3169+floehopper@users.noreply.github.com> Date: Sat, 15 Dec 2018 11:35:39 +0000 Subject: [PATCH 09/57] Create an EventRule to fire at daily at 2am This rule currently has no target and so won't do anything, but it's a small step on the way. --- index.ts | 6 ++++++ package-lock.json | 17 +++++++++++++++++ package.json | 1 + 3 files changed, 24 insertions(+) diff --git a/index.ts b/index.ts index a52410e..03534f5 100644 --- a/index.ts +++ b/index.ts @@ -1,8 +1,14 @@ import cdk = require('@aws-cdk/cdk'); +import events = require('@aws-cdk/aws-events'); class TrelloBackupStack extends cdk.Stack { constructor(parent: cdk.App, id: string, props?: cdk.StackProps) { super(parent, id, props); + + const dailyAt2am = 'cron(0 2 * * ? *)'; + const dailyAt2amRule = new events.EventRule(this, 'DailyAt2amRule', { + scheduleExpression: dailyAt2am, + }); } } diff --git a/package-lock.json b/package-lock.json index 936a6ca..85b4cbc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,6 +15,23 @@ "yaml": "^1.1.0" } }, + "@aws-cdk/aws-events": { + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/@aws-cdk/aws-events/-/aws-events-0.20.0.tgz", + "integrity": "sha512-4LyROSw+mBZrrtJGjUvDP91M0Hq18VDUppxRbwjTdESGhHMogSQfB6j0xsctswxR1Hv4jF1H6ZlhxQlrQlXucg==", + "requires": { + "@aws-cdk/aws-iam": "^0.20.0", + "@aws-cdk/cdk": "^0.20.0" + } + }, + "@aws-cdk/aws-iam": { + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/@aws-cdk/aws-iam/-/aws-iam-0.20.0.tgz", + "integrity": "sha512-fl6n5zAN5S/stry6+ij8Ziu60J/C6oUA9DWrsYRAUzNpZNIr2Cgo8tv0SrgAV/t7635ged5YtSzlHj1Gdc1bjw==", + "requires": { + "@aws-cdk/cdk": "^0.20.0" + } + }, "@aws-cdk/cdk": { "version": "0.20.0", "resolved": "https://registry.npmjs.org/@aws-cdk/cdk/-/cdk-0.20.0.tgz", diff --git a/package.json b/package.json index 86d1bd9..98af8c8 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ "author": "Go Free Range Ltd", "license": "ISC", "dependencies": { + "@aws-cdk/aws-events": "^0.20.0", "aws-cdk": "^0.20.0", "typescript": "^3.2.2" } From 2cabad250a3f5411ac9edd9aea862ba092c0eab9 Mon Sep 17 00:00:00 2001 From: James Mead <3169+floehopper@users.noreply.github.com> Date: Sat, 15 Dec 2018 12:12:31 +0000 Subject: [PATCH 10/57] Add @aws-cdk/aws-lambda package --- package-lock.json | 131 ++++++++++++++++++++++++++++++++++++++++++++++ package.json | 1 + 2 files changed, 132 insertions(+) diff --git a/package-lock.json b/package-lock.json index 85b4cbc..120d4f8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,6 +15,55 @@ "yaml": "^1.1.0" } }, + "@aws-cdk/assets": { + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/@aws-cdk/assets/-/assets-0.20.0.tgz", + "integrity": "sha512-6M2XjU0gddZchFP9WkwGOM21usK82M1PUC/3ev/mlTh8xM83FjYGZeHTS1asdhC3EG1dhflNZ+AmJq/M7JpGxQ==", + "requires": { + "@aws-cdk/aws-iam": "^0.20.0", + "@aws-cdk/aws-s3": "^0.20.0", + "@aws-cdk/cdk": "^0.20.0", + "@aws-cdk/cx-api": "^0.20.0" + } + }, + "@aws-cdk/aws-autoscaling-api": { + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/@aws-cdk/aws-autoscaling-api/-/aws-autoscaling-api-0.20.0.tgz", + "integrity": "sha512-zQEP/Y8byrJ9QJdraESsD+ei25bDr1zEv6Fiz/7ess1wG3XfZPNRzNeFBfNF3ynqH0ktp6ZJSLnzmbgVeD2yNA==", + "requires": { + "@aws-cdk/aws-iam": "^0.20.0", + "@aws-cdk/cdk": "^0.20.0" + } + }, + "@aws-cdk/aws-cloudwatch": { + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/@aws-cdk/aws-cloudwatch/-/aws-cloudwatch-0.20.0.tgz", + "integrity": "sha512-ChOEJS5J5xs2B669UpNhytSYK+TC6rW15j+4Ne/D95no7PFSmwdSCifNu+kQKtT2hmHCWSSdZxQQl828HQXPYQ==", + "requires": { + "@aws-cdk/aws-iam": "^0.20.0", + "@aws-cdk/cdk": "^0.20.0" + } + }, + "@aws-cdk/aws-codepipeline-api": { + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/@aws-cdk/aws-codepipeline-api/-/aws-codepipeline-api-0.20.0.tgz", + "integrity": "sha512-w9AkV51/Bx1FyINCBEsAu5OEkc5HTybgxvSzzPgBH/xcuOgazCtTAY+n+2huBc4wO6jM3MshAQccCcuWOxSb8Q==", + "requires": { + "@aws-cdk/aws-events": "^0.20.0", + "@aws-cdk/aws-iam": "^0.20.0", + "@aws-cdk/cdk": "^0.20.0" + } + }, + "@aws-cdk/aws-ec2": { + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/@aws-cdk/aws-ec2/-/aws-ec2-0.20.0.tgz", + "integrity": "sha512-V1Ez1S1eMAzHuiULXQk49P7lCyrS6UD5DzRVb6mBAkX1Kv65MyLpeiHNhmRd5aa/x9cK0LhXNZPrzDZ4ShSm4A==", + "requires": { + "@aws-cdk/aws-iam": "^0.20.0", + "@aws-cdk/cdk": "^0.20.0", + "@aws-cdk/cx-api": "^0.20.0" + } + }, "@aws-cdk/aws-events": { "version": "0.20.0", "resolved": "https://registry.npmjs.org/@aws-cdk/aws-events/-/aws-events-0.20.0.tgz", @@ -32,6 +81,88 @@ "@aws-cdk/cdk": "^0.20.0" } }, + "@aws-cdk/aws-kms": { + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/@aws-cdk/aws-kms/-/aws-kms-0.20.0.tgz", + "integrity": "sha512-cLRakK/CK14+1Hhhll75qra70nKJNzTCPZ9+/ZJY8Xc9zO3fDJWejF7+ZapwjN6kI8QGkAwOx+cK9dJ5KnvPzw==", + "requires": { + "@aws-cdk/aws-iam": "^0.20.0", + "@aws-cdk/cdk": "^0.20.0" + } + }, + "@aws-cdk/aws-lambda": { + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/@aws-cdk/aws-lambda/-/aws-lambda-0.20.0.tgz", + "integrity": "sha512-tlRBkqZBzR+C6VxJDhM41S6Qf6y0SSQ7PlNyH58Cqze0IPVBYO1loKh/e2KCyQRofR1YlAPRkNqev17qkx6dXA==", + "requires": { + "@aws-cdk/assets": "^0.20.0", + "@aws-cdk/aws-cloudwatch": "^0.20.0", + "@aws-cdk/aws-codepipeline-api": "^0.20.0", + "@aws-cdk/aws-ec2": "^0.20.0", + "@aws-cdk/aws-events": "^0.20.0", + "@aws-cdk/aws-iam": "^0.20.0", + "@aws-cdk/aws-logs": "^0.20.0", + "@aws-cdk/aws-s3": "^0.20.0", + "@aws-cdk/aws-s3-notifications": "^0.20.0", + "@aws-cdk/aws-sqs": "^0.20.0", + "@aws-cdk/aws-stepfunctions": "^0.20.0", + "@aws-cdk/cdk": "^0.20.0", + "@aws-cdk/cx-api": "^0.20.0" + } + }, + "@aws-cdk/aws-logs": { + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/@aws-cdk/aws-logs/-/aws-logs-0.20.0.tgz", + "integrity": "sha512-WONAOMZ2sBsAvSEZ0YYhAbz6GSWmAJi1Goo/0wyDArONORAELeRzXRqyXCQk+zlqMsnETpOaCopgsOG2aDYHXA==", + "requires": { + "@aws-cdk/aws-cloudwatch": "^0.20.0", + "@aws-cdk/aws-iam": "^0.20.0", + "@aws-cdk/cdk": "^0.20.0" + } + }, + "@aws-cdk/aws-s3": { + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/@aws-cdk/aws-s3/-/aws-s3-0.20.0.tgz", + "integrity": "sha512-JFjcimPa20fpm8XPvhbSrsSFnEUNFqMDzUWhjOAWckkS6KQ08BVJJiY/06Nz/w8gG8y2IvtWsQpLZUPN+W7sZA==", + "requires": { + "@aws-cdk/aws-codepipeline-api": "^0.20.0", + "@aws-cdk/aws-iam": "^0.20.0", + "@aws-cdk/aws-kms": "^0.20.0", + "@aws-cdk/aws-s3-notifications": "^0.20.0", + "@aws-cdk/cdk": "^0.20.0" + } + }, + "@aws-cdk/aws-s3-notifications": { + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/@aws-cdk/aws-s3-notifications/-/aws-s3-notifications-0.20.0.tgz", + "integrity": "sha512-2jiEhS4qOCfsQcPax+xAb3icXcyApRClnzEOzPfnhxnWhH6T19r4D7MWnha7c3cjc9InkkEksGvXFzTvdPSKKQ==", + "requires": { + "@aws-cdk/cdk": "^0.20.0" + } + }, + "@aws-cdk/aws-sqs": { + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/@aws-cdk/aws-sqs/-/aws-sqs-0.20.0.tgz", + "integrity": "sha512-c8dCcVOFoWAqc8EUqav2NMRZD9AkGth9bYDjTdYnxG3H9eOmaYiv5IWRrLhTPWdPc0VCy9cGGK2lAtf+isBr4w==", + "requires": { + "@aws-cdk/aws-autoscaling-api": "^0.20.0", + "@aws-cdk/aws-iam": "^0.20.0", + "@aws-cdk/aws-kms": "^0.20.0", + "@aws-cdk/aws-s3-notifications": "^0.20.0", + "@aws-cdk/cdk": "^0.20.0" + } + }, + "@aws-cdk/aws-stepfunctions": { + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/@aws-cdk/aws-stepfunctions/-/aws-stepfunctions-0.20.0.tgz", + "integrity": "sha512-VS5dcMQ+VAm1WMoAYt4p5K5crBw511qPOULmBwWRnHAezUPlJgtqKgev0ffNTpA+g1RTQP4ZoTaHpIFISi+wZA==", + "requires": { + "@aws-cdk/aws-cloudwatch": "^0.20.0", + "@aws-cdk/aws-events": "^0.20.0", + "@aws-cdk/aws-iam": "^0.20.0", + "@aws-cdk/cdk": "^0.20.0" + } + }, "@aws-cdk/cdk": { "version": "0.20.0", "resolved": "https://registry.npmjs.org/@aws-cdk/cdk/-/cdk-0.20.0.tgz", diff --git a/package.json b/package.json index 98af8c8..19ee92b 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ "license": "ISC", "dependencies": { "@aws-cdk/aws-events": "^0.20.0", + "@aws-cdk/aws-lambda": "^0.20.0", "aws-cdk": "^0.20.0", "typescript": "^3.2.2" } From fd3e64d873ab257812b5908528cfb327fc07176a Mon Sep 17 00:00:00 2001 From: James Mead <3169+floehopper@users.noreply.github.com> Date: Sat, 15 Dec 2018 12:20:25 +0000 Subject: [PATCH 11/57] Add empty Ruby lambda function as target of scheduled rule Note that Ruby lambda functions cannot currently be "inline"; they have to be in a separate file. --- README.md | 4 ++++ index.ts | 9 +++++++++ lambdaFunctions/enumerateTrelloBoards/index.rb | 3 +++ 3 files changed, 16 insertions(+) create mode 100644 lambdaFunctions/enumerateTrelloBoards/index.rb diff --git a/README.md b/README.md index bdb8cc3..a42662a 100644 --- a/README.md +++ b/README.md @@ -36,5 +36,9 @@ there is no need to specify this unless you want to override that value. * `node_modules/.bin` must be in your `PATH` environment variable in order to be able to run the `cdk` command-line tool. +* When a stack uses assets (e.g. via a call to `aws-lambda.Code.asset()`), +before using the `cdk` command-line tool to deploy to an AWS environment for +the first time, the environment must be bootstrapped using: `cdk bootstrap`. + [1]: https://awslabs.github.io/aws-cdk/getting-started.html#configuring-the-cdk-toolkit [2]: https://awslabs.github.io/aws-cdk/tools.html#command-line-toolkit-cdk diff --git a/index.ts b/index.ts index 03534f5..f4c4ccb 100644 --- a/index.ts +++ b/index.ts @@ -1,14 +1,23 @@ import cdk = require('@aws-cdk/cdk'); +import lambda = require('@aws-cdk/aws-lambda'); import events = require('@aws-cdk/aws-events'); class TrelloBackupStack extends cdk.Stack { constructor(parent: cdk.App, id: string, props?: cdk.StackProps) { super(parent, id, props); + const enumerateTrelloBoards = new lambda.Function(this, 'enumerateTrelloBoards', { + runtime: new lambda.Runtime('ruby2.5'), + handler: 'index.handler', + code: lambda.Code.asset('./lambdaFunctions/enumerateTrelloBoards'), + timeout: 30 + }); + const dailyAt2am = 'cron(0 2 * * ? *)'; const dailyAt2amRule = new events.EventRule(this, 'DailyAt2amRule', { scheduleExpression: dailyAt2am, }); + dailyAt2amRule.addTarget(enumerateTrelloBoards); } } diff --git a/lambdaFunctions/enumerateTrelloBoards/index.rb b/lambdaFunctions/enumerateTrelloBoards/index.rb new file mode 100644 index 0000000..93cb29c --- /dev/null +++ b/lambdaFunctions/enumerateTrelloBoards/index.rb @@ -0,0 +1,3 @@ +def handler(event:, context:) + { statusCode: 200, body: 'OK' } +end From 0ea4cd7d1a5192bf3fd506977bcc078629723c26 Mon Sep 17 00:00:00 2001 From: James Mead <3169+floehopper@users.noreply.github.com> Date: Sat, 15 Dec 2018 12:33:24 +0000 Subject: [PATCH 12/57] Add empty Gemfile, constrain Ruby version & vendor gems --- lambdaFunctions/build.sh | 2 ++ lambdaFunctions/enumerateTrelloBoards/.gitignore | 1 + lambdaFunctions/enumerateTrelloBoards/Gemfile | 3 +++ lambdaFunctions/enumerateTrelloBoards/Gemfile.lock | 14 ++++++++++++++ package.json | 3 ++- 5 files changed, 22 insertions(+), 1 deletion(-) create mode 100755 lambdaFunctions/build.sh create mode 100644 lambdaFunctions/enumerateTrelloBoards/.gitignore create mode 100644 lambdaFunctions/enumerateTrelloBoards/Gemfile create mode 100644 lambdaFunctions/enumerateTrelloBoards/Gemfile.lock diff --git a/lambdaFunctions/build.sh b/lambdaFunctions/build.sh new file mode 100755 index 0000000..c71131a --- /dev/null +++ b/lambdaFunctions/build.sh @@ -0,0 +1,2 @@ +#!/usr/bin/env bash +cd enumerateTrelloBoards && bundle install --path vendor/bundle diff --git a/lambdaFunctions/enumerateTrelloBoards/.gitignore b/lambdaFunctions/enumerateTrelloBoards/.gitignore new file mode 100644 index 0000000..22d0d82 --- /dev/null +++ b/lambdaFunctions/enumerateTrelloBoards/.gitignore @@ -0,0 +1 @@ +vendor diff --git a/lambdaFunctions/enumerateTrelloBoards/Gemfile b/lambdaFunctions/enumerateTrelloBoards/Gemfile new file mode 100644 index 0000000..767e2a1 --- /dev/null +++ b/lambdaFunctions/enumerateTrelloBoards/Gemfile @@ -0,0 +1,3 @@ +source 'https://rubygems.org' + +ruby '~> 2.5.0' diff --git a/lambdaFunctions/enumerateTrelloBoards/Gemfile.lock b/lambdaFunctions/enumerateTrelloBoards/Gemfile.lock new file mode 100644 index 0000000..47c816b --- /dev/null +++ b/lambdaFunctions/enumerateTrelloBoards/Gemfile.lock @@ -0,0 +1,14 @@ +GEM + remote: https://rubygems.org/ + specs: + +PLATFORMS + ruby + +DEPENDENCIES + +RUBY VERSION + ruby 2.5.3p105 + +BUNDLED WITH + 1.17.1 diff --git a/package.json b/package.json index 19ee92b..42d76f4 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,8 @@ "description": "Backup Trello boards to S3", "main": "index.js", "scripts": { - "build": "tsc" + "buildLambdaFunctions": "cd lambdaFunctions && ./build.sh", + "build": "tsc && npm run buildLambdaFunctions" }, "author": "Go Free Range Ltd", "license": "ISC", From 0c33a8c28ba13832f91075a61bea02b4575a9a62 Mon Sep 17 00:00:00 2001 From: James Mead <3169+floehopper@users.noreply.github.com> Date: Sat, 15 Dec 2018 12:38:39 +0000 Subject: [PATCH 13/57] Extract build.sh for individual lambda function --- lambdaFunctions/build.sh | 2 +- lambdaFunctions/enumerateTrelloBoards/build.sh | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) create mode 100755 lambdaFunctions/enumerateTrelloBoards/build.sh diff --git a/lambdaFunctions/build.sh b/lambdaFunctions/build.sh index c71131a..a8341db 100755 --- a/lambdaFunctions/build.sh +++ b/lambdaFunctions/build.sh @@ -1,2 +1,2 @@ #!/usr/bin/env bash -cd enumerateTrelloBoards && bundle install --path vendor/bundle +cd enumerateTrelloBoards && ./build.sh diff --git a/lambdaFunctions/enumerateTrelloBoards/build.sh b/lambdaFunctions/enumerateTrelloBoards/build.sh new file mode 100755 index 0000000..b9f73a9 --- /dev/null +++ b/lambdaFunctions/enumerateTrelloBoards/build.sh @@ -0,0 +1,2 @@ +#!/usr/bin/env bash +bundle install --path vendor/bundle From 182f3f891edba3fca3672aa3ddf8724f06aed652 Mon Sep 17 00:00:00 2001 From: James Mead <3169+floehopper@users.noreply.github.com> Date: Sat, 15 Dec 2018 12:39:55 +0000 Subject: [PATCH 14/57] Make lambda function build scripts fail if any command fails --- lambdaFunctions/build.sh | 1 + lambdaFunctions/enumerateTrelloBoards/build.sh | 1 + 2 files changed, 2 insertions(+) diff --git a/lambdaFunctions/build.sh b/lambdaFunctions/build.sh index a8341db..43acb80 100755 --- a/lambdaFunctions/build.sh +++ b/lambdaFunctions/build.sh @@ -1,2 +1,3 @@ #!/usr/bin/env bash +set -e cd enumerateTrelloBoards && ./build.sh diff --git a/lambdaFunctions/enumerateTrelloBoards/build.sh b/lambdaFunctions/enumerateTrelloBoards/build.sh index b9f73a9..41eca98 100755 --- a/lambdaFunctions/enumerateTrelloBoards/build.sh +++ b/lambdaFunctions/enumerateTrelloBoards/build.sh @@ -1,2 +1,3 @@ #!/usr/bin/env bash +set -e bundle install --path vendor/bundle From c78db03d219dfeb5a81fcd8d2cd63ab9912bb0cd Mon Sep 17 00:00:00 2001 From: James Mead <3169+floehopper@users.noreply.github.com> Date: Sat, 15 Dec 2018 12:50:01 +0000 Subject: [PATCH 15/57] Make dotenv gem available for enumerateTrelloBoards lambda function --- lambdaFunctions/enumerateTrelloBoards/.gitignore | 1 + lambdaFunctions/enumerateTrelloBoards/Gemfile | 2 ++ lambdaFunctions/enumerateTrelloBoards/Gemfile.lock | 2 ++ lambdaFunctions/enumerateTrelloBoards/index.rb | 4 ++++ 4 files changed, 9 insertions(+) diff --git a/lambdaFunctions/enumerateTrelloBoards/.gitignore b/lambdaFunctions/enumerateTrelloBoards/.gitignore index 22d0d82..226ca36 100644 --- a/lambdaFunctions/enumerateTrelloBoards/.gitignore +++ b/lambdaFunctions/enumerateTrelloBoards/.gitignore @@ -1 +1,2 @@ +.env vendor diff --git a/lambdaFunctions/enumerateTrelloBoards/Gemfile b/lambdaFunctions/enumerateTrelloBoards/Gemfile index 767e2a1..ce092d1 100644 --- a/lambdaFunctions/enumerateTrelloBoards/Gemfile +++ b/lambdaFunctions/enumerateTrelloBoards/Gemfile @@ -1,3 +1,5 @@ source 'https://rubygems.org' ruby '~> 2.5.0' + +gem 'dotenv' diff --git a/lambdaFunctions/enumerateTrelloBoards/Gemfile.lock b/lambdaFunctions/enumerateTrelloBoards/Gemfile.lock index 47c816b..b2a9648 100644 --- a/lambdaFunctions/enumerateTrelloBoards/Gemfile.lock +++ b/lambdaFunctions/enumerateTrelloBoards/Gemfile.lock @@ -1,11 +1,13 @@ GEM remote: https://rubygems.org/ specs: + dotenv (2.5.0) PLATFORMS ruby DEPENDENCIES + dotenv RUBY VERSION ruby 2.5.3p105 diff --git a/lambdaFunctions/enumerateTrelloBoards/index.rb b/lambdaFunctions/enumerateTrelloBoards/index.rb index 93cb29c..fb6961b 100644 --- a/lambdaFunctions/enumerateTrelloBoards/index.rb +++ b/lambdaFunctions/enumerateTrelloBoards/index.rb @@ -1,3 +1,7 @@ +require 'dotenv' + +Dotenv.load + def handler(event:, context:) { statusCode: 200, body: 'OK' } end From 1e42cf7d3ec27e8aa8b9c90093854a7b87bc3fe8 Mon Sep 17 00:00:00 2001 From: James Mead <3169+floehopper@users.noreply.github.com> Date: Sat, 15 Dec 2018 12:50:49 +0000 Subject: [PATCH 16/57] Document obtaining Trello API key & token and print in lambda function Note this is closely based on the original instructions [1]. [1]: https://github.com/freerange/trello_backup/blob/a99bacf2f663c899a76947dd91810c6ee353d429/README.md#generatingstoring-the-tokens-you-need-to-use-this-script --- README.md | 28 +++++++++++++++++++ .../enumerateTrelloBoards/index.rb | 3 ++ 2 files changed, 31 insertions(+) diff --git a/README.md b/README.md index a42662a..9d08846 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,34 @@ command-line tool. See [Configuring the AWS CDK Toolkit][1] for details. to setup a named profile and then use the `--profile ` option with the `cdk` command-line tool to select the relevant AWS profile. +### Getting a Trello API key + +Ensure you're logged in as the GFR Admin user. + + $ open "https://trello.com/1/appKey/generate" + # Copy the Key (under Developer API Keys) to the clipboard + + # Temporarily store the Trello API key in an environment variable + $ export TRELLO_KEY=`pbpaste` + + # Store the Trello API key in .env + $ echo "TRELLO_KEY=$TRELLO_KEY" >> lambdaFunctions/enumerateTrelloBoards/.env + +### Getting a Trello API token + +Ensure you're logged in as the GFR Admin user on https://trello.com. It's safe to run do this step multiple times as you'll get the same token back even if it's already been generated. + + $ open "https://trello.com/1/connect?key=$TRELLO_KEY&name=gfr-trello-backup&expiration=never&response_type=token" + # Allow the gfr-trello-backup app to read our Trello account + + # Copy the token from the resulting page + + # Temporarily store the Trello token in an environment variable + $ export TRELLO_TOKEN=`pbpaste` + + # Store the Trello token in .env + $ echo "TRELLO_TOKEN=$TRELLO_TOKEN" >> lambdaFunctions/enumerateTrelloBoards/.env + ## Build npm run build diff --git a/lambdaFunctions/enumerateTrelloBoards/index.rb b/lambdaFunctions/enumerateTrelloBoards/index.rb index fb6961b..2ebdf5a 100644 --- a/lambdaFunctions/enumerateTrelloBoards/index.rb +++ b/lambdaFunctions/enumerateTrelloBoards/index.rb @@ -2,6 +2,9 @@ Dotenv.load +p ENV.fetch('TRELLO_KEY') +p ENV.fetch('TRELLO_TOKEN') + def handler(event:, context:) { statusCode: 200, body: 'OK' } end From 52b61f1c7827bdbbb065589ff94eecaca2080423 Mon Sep 17 00:00:00 2001 From: James Mead <3169+floehopper@users.noreply.github.com> Date: Sat, 15 Dec 2018 13:56:33 +0000 Subject: [PATCH 17/57] Use trello gem to print name of first board Note that unfortunately the trello gem currently (rather unnecessarily) has an indirect dependency on the unf gem which has native extensions. When I was trying this previously, I thought I ran into problems because the extensions were being built on my local machine using Mac OSX (x86_64-darwin-17), but were being deployed and run in an AWS Lambda function which is an x86_64-linux platform. Previously I had used Docker to build the extensions for the correct platform, however, when I just tried deploying and running the function as it stands (with Mac OSX-build extensions), it seemed to work OK, so maybe the whole Docker thing was unnecessary! --- lambdaFunctions/enumerateTrelloBoards/Gemfile | 1 + .../enumerateTrelloBoards/Gemfile.lock | 41 +++++++++++++++++++ .../enumerateTrelloBoards/index.rb | 8 +++- 3 files changed, 48 insertions(+), 2 deletions(-) diff --git a/lambdaFunctions/enumerateTrelloBoards/Gemfile b/lambdaFunctions/enumerateTrelloBoards/Gemfile index ce092d1..fca0b2e 100644 --- a/lambdaFunctions/enumerateTrelloBoards/Gemfile +++ b/lambdaFunctions/enumerateTrelloBoards/Gemfile @@ -3,3 +3,4 @@ source 'https://rubygems.org' ruby '~> 2.5.0' gem 'dotenv' +gem 'ruby-trello' diff --git a/lambdaFunctions/enumerateTrelloBoards/Gemfile.lock b/lambdaFunctions/enumerateTrelloBoards/Gemfile.lock index b2a9648..f0fcfad 100644 --- a/lambdaFunctions/enumerateTrelloBoards/Gemfile.lock +++ b/lambdaFunctions/enumerateTrelloBoards/Gemfile.lock @@ -1,13 +1,54 @@ GEM remote: https://rubygems.org/ specs: + activemodel (5.2.2) + activesupport (= 5.2.2) + activesupport (5.2.2) + concurrent-ruby (~> 1.0, >= 1.0.2) + i18n (>= 0.7, < 2) + minitest (~> 5.1) + tzinfo (~> 1.1) + addressable (2.5.2) + public_suffix (>= 2.0.2, < 4.0) + concurrent-ruby (1.1.3) + domain_name (0.5.20180417) + unf (>= 0.0.5, < 1.0.0) dotenv (2.5.0) + http-cookie (1.0.3) + domain_name (~> 0.5) + i18n (1.2.0) + concurrent-ruby (~> 1.0) + json (2.1.0) + mime-types (3.2.2) + mime-types-data (~> 3.2015) + mime-types-data (3.2018.0812) + minitest (5.11.3) + netrc (0.11.0) + oauth (0.5.4) + public_suffix (3.0.3) + rest-client (2.0.2) + http-cookie (>= 1.0.2, < 2.0) + mime-types (>= 1.16, < 4.0) + netrc (~> 0.8) + ruby-trello (2.1.0) + activemodel (>= 3.2.0) + addressable (~> 2.3) + json + oauth (>= 0.4.5) + rest-client (>= 1.8.0) + thread_safe (0.3.6) + tzinfo (1.2.5) + thread_safe (~> 0.1) + unf (0.1.4) + unf_ext + unf_ext (0.0.7.5) PLATFORMS ruby DEPENDENCIES dotenv + ruby-trello RUBY VERSION ruby 2.5.3p105 diff --git a/lambdaFunctions/enumerateTrelloBoards/index.rb b/lambdaFunctions/enumerateTrelloBoards/index.rb index 2ebdf5a..086b7d0 100644 --- a/lambdaFunctions/enumerateTrelloBoards/index.rb +++ b/lambdaFunctions/enumerateTrelloBoards/index.rb @@ -1,10 +1,14 @@ require 'dotenv' +require 'trello' Dotenv.load -p ENV.fetch('TRELLO_KEY') -p ENV.fetch('TRELLO_TOKEN') +Trello.configure do |config| + config.developer_public_key = ENV.fetch('TRELLO_KEY') + config.member_token = ENV.fetch('TRELLO_TOKEN') +end def handler(event:, context:) + p Trello::Board.all.first.name { statusCode: 200, body: 'OK' } end From 23f6a40fa0a2569f2daa520e1403ba008dc936ab Mon Sep 17 00:00:00 2001 From: James Mead <3169+floehopper@users.noreply.github.com> Date: Sat, 15 Dec 2018 14:16:04 +0000 Subject: [PATCH 18/57] Add @aws-cdk/aws-sns package --- package-lock.json | 15 +++++++++++++++ package.json | 1 + 2 files changed, 16 insertions(+) diff --git a/package-lock.json b/package-lock.json index 120d4f8..2a21a69 100644 --- a/package-lock.json +++ b/package-lock.json @@ -140,6 +140,21 @@ "@aws-cdk/cdk": "^0.20.0" } }, + "@aws-cdk/aws-sns": { + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/@aws-cdk/aws-sns/-/aws-sns-0.20.0.tgz", + "integrity": "sha512-28pN8oKIT6b4RwEgG+HkrOU6DxWp//6Vh+z7JQfG4jCvbTEyqKAo6pEnEExJMBAhfQDjln1CKDMbBlhXG9wBfA==", + "requires": { + "@aws-cdk/aws-autoscaling-api": "^0.20.0", + "@aws-cdk/aws-cloudwatch": "^0.20.0", + "@aws-cdk/aws-events": "^0.20.0", + "@aws-cdk/aws-iam": "^0.20.0", + "@aws-cdk/aws-lambda": "^0.20.0", + "@aws-cdk/aws-s3-notifications": "^0.20.0", + "@aws-cdk/aws-sqs": "^0.20.0", + "@aws-cdk/cdk": "^0.20.0" + } + }, "@aws-cdk/aws-sqs": { "version": "0.20.0", "resolved": "https://registry.npmjs.org/@aws-cdk/aws-sqs/-/aws-sqs-0.20.0.tgz", diff --git a/package.json b/package.json index 42d76f4..e961243 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "dependencies": { "@aws-cdk/aws-events": "^0.20.0", "@aws-cdk/aws-lambda": "^0.20.0", + "@aws-cdk/aws-sns": "^0.20.0", "aws-cdk": "^0.20.0", "typescript": "^3.2.2" } From 03513a1491d22678c8f7fd0a7febaf14562b2ca8 Mon Sep 17 00:00:00 2001 From: James Mead <3169+floehopper@users.noreply.github.com> Date: Sat, 15 Dec 2018 14:23:12 +0000 Subject: [PATCH 19/57] Create SNS Topic and publish message to it for each Trello board --- index.ts | 8 +++++++ lambdaFunctions/enumerateTrelloBoards/Gemfile | 1 + .../enumerateTrelloBoards/Gemfile.lock | 13 +++++++++++ .../enumerateTrelloBoards/index.rb | 23 ++++++++++++++++++- 4 files changed, 44 insertions(+), 1 deletion(-) diff --git a/index.ts b/index.ts index f4c4ccb..6cdd5e8 100644 --- a/index.ts +++ b/index.ts @@ -1,18 +1,26 @@ import cdk = require('@aws-cdk/cdk'); import lambda = require('@aws-cdk/aws-lambda'); import events = require('@aws-cdk/aws-events'); +import { Topic } from '@aws-cdk/aws-sns'; class TrelloBackupStack extends cdk.Stack { constructor(parent: cdk.App, id: string, props?: cdk.StackProps) { super(parent, id, props); + const backupTrelloBoardTopic = new Topic(this, 'backupTrelloBoardTopic'); + const enumerateTrelloBoards = new lambda.Function(this, 'enumerateTrelloBoards', { runtime: new lambda.Runtime('ruby2.5'), handler: 'index.handler', code: lambda.Code.asset('./lambdaFunctions/enumerateTrelloBoards'), + environment: { + BACKUP_TRELLO_BOARD_TOPIC_ARN: backupTrelloBoardTopic.topicArn + }, timeout: 30 }); + backupTrelloBoardTopic.grantPublish(enumerateTrelloBoards.role); + const dailyAt2am = 'cron(0 2 * * ? *)'; const dailyAt2amRule = new events.EventRule(this, 'DailyAt2amRule', { scheduleExpression: dailyAt2am, diff --git a/lambdaFunctions/enumerateTrelloBoards/Gemfile b/lambdaFunctions/enumerateTrelloBoards/Gemfile index fca0b2e..2123d97 100644 --- a/lambdaFunctions/enumerateTrelloBoards/Gemfile +++ b/lambdaFunctions/enumerateTrelloBoards/Gemfile @@ -2,5 +2,6 @@ source 'https://rubygems.org' ruby '~> 2.5.0' +gem 'aws-sdk-sns' gem 'dotenv' gem 'ruby-trello' diff --git a/lambdaFunctions/enumerateTrelloBoards/Gemfile.lock b/lambdaFunctions/enumerateTrelloBoards/Gemfile.lock index f0fcfad..3e12e4d 100644 --- a/lambdaFunctions/enumerateTrelloBoards/Gemfile.lock +++ b/lambdaFunctions/enumerateTrelloBoards/Gemfile.lock @@ -10,6 +10,17 @@ GEM tzinfo (~> 1.1) addressable (2.5.2) public_suffix (>= 2.0.2, < 4.0) + aws-eventstream (1.0.1) + aws-partitions (1.125.0) + aws-sdk-core (3.44.0) + aws-eventstream (~> 1.0) + aws-partitions (~> 1.0) + aws-sigv4 (~> 1.0) + jmespath (~> 1.0) + aws-sdk-sns (1.9.0) + aws-sdk-core (~> 3, >= 3.39.0) + aws-sigv4 (~> 1.0) + aws-sigv4 (1.0.3) concurrent-ruby (1.1.3) domain_name (0.5.20180417) unf (>= 0.0.5, < 1.0.0) @@ -18,6 +29,7 @@ GEM domain_name (~> 0.5) i18n (1.2.0) concurrent-ruby (~> 1.0) + jmespath (1.4.0) json (2.1.0) mime-types (3.2.2) mime-types-data (~> 3.2015) @@ -47,6 +59,7 @@ PLATFORMS ruby DEPENDENCIES + aws-sdk-sns dotenv ruby-trello diff --git a/lambdaFunctions/enumerateTrelloBoards/index.rb b/lambdaFunctions/enumerateTrelloBoards/index.rb index 086b7d0..e030939 100644 --- a/lambdaFunctions/enumerateTrelloBoards/index.rb +++ b/lambdaFunctions/enumerateTrelloBoards/index.rb @@ -1,3 +1,4 @@ +require 'aws-sdk-sns' require 'dotenv' require 'trello' @@ -8,7 +9,27 @@ config.member_token = ENV.fetch('TRELLO_TOKEN') end +TOPIC_ARN = ENV.fetch('BACKUP_TRELLO_BOARD_TOPIC_ARN') + def handler(event:, context:) - p Trello::Board.all.first.name + sns = Aws::SNS::Client.new + boards = Trello::Board.all + boards.each do |board| + sns.publish( + topic_arn: TOPIC_ARN, + subject: "Backup Trello board", + message: "Backup Trello board: #{board.name}", + message_attributes: { + 'board_id' => { + data_type: 'String', + string_value: board.id + }, + 'board_name' => { + data_type: 'String', + string_value: board.name + } + } + ) + end { statusCode: 200, body: 'OK' } end From 1a5b2738f558e41e575e4a5d36f252ff7d10a615 Mon Sep 17 00:00:00 2001 From: James Mead <3169+floehopper@users.noreply.github.com> Date: Sat, 15 Dec 2018 14:28:18 +0000 Subject: [PATCH 20/57] Subscribe 2nd lambda function to SNS Topic and print events --- index.ts | 8 ++++++++ lambdaFunctions/backupTrelloBoard/.gitignore | 1 + lambdaFunctions/backupTrelloBoard/Gemfile | 3 +++ lambdaFunctions/backupTrelloBoard/Gemfile.lock | 14 ++++++++++++++ lambdaFunctions/backupTrelloBoard/build.sh | 3 +++ lambdaFunctions/backupTrelloBoard/index.rb | 4 ++++ lambdaFunctions/build.sh | 2 +- 7 files changed, 34 insertions(+), 1 deletion(-) create mode 100644 lambdaFunctions/backupTrelloBoard/.gitignore create mode 100644 lambdaFunctions/backupTrelloBoard/Gemfile create mode 100644 lambdaFunctions/backupTrelloBoard/Gemfile.lock create mode 100755 lambdaFunctions/backupTrelloBoard/build.sh create mode 100644 lambdaFunctions/backupTrelloBoard/index.rb diff --git a/index.ts b/index.ts index 6cdd5e8..46a96d3 100644 --- a/index.ts +++ b/index.ts @@ -19,7 +19,15 @@ class TrelloBackupStack extends cdk.Stack { timeout: 30 }); + const backupTrelloBoard = new lambda.Function(this, 'backupTrelloBoard', { + runtime: new lambda.Runtime('ruby2.5'), + handler: 'index.handler', + code: lambda.Code.asset('./lambdaFunctions/backupTrelloBoard'), + timeout: 30 + }); + backupTrelloBoardTopic.grantPublish(enumerateTrelloBoards.role); + backupTrelloBoardTopic.subscribeLambda(backupTrelloBoard); const dailyAt2am = 'cron(0 2 * * ? *)'; const dailyAt2amRule = new events.EventRule(this, 'DailyAt2amRule', { diff --git a/lambdaFunctions/backupTrelloBoard/.gitignore b/lambdaFunctions/backupTrelloBoard/.gitignore new file mode 100644 index 0000000..22d0d82 --- /dev/null +++ b/lambdaFunctions/backupTrelloBoard/.gitignore @@ -0,0 +1 @@ +vendor diff --git a/lambdaFunctions/backupTrelloBoard/Gemfile b/lambdaFunctions/backupTrelloBoard/Gemfile new file mode 100644 index 0000000..767e2a1 --- /dev/null +++ b/lambdaFunctions/backupTrelloBoard/Gemfile @@ -0,0 +1,3 @@ +source 'https://rubygems.org' + +ruby '~> 2.5.0' diff --git a/lambdaFunctions/backupTrelloBoard/Gemfile.lock b/lambdaFunctions/backupTrelloBoard/Gemfile.lock new file mode 100644 index 0000000..47c816b --- /dev/null +++ b/lambdaFunctions/backupTrelloBoard/Gemfile.lock @@ -0,0 +1,14 @@ +GEM + remote: https://rubygems.org/ + specs: + +PLATFORMS + ruby + +DEPENDENCIES + +RUBY VERSION + ruby 2.5.3p105 + +BUNDLED WITH + 1.17.1 diff --git a/lambdaFunctions/backupTrelloBoard/build.sh b/lambdaFunctions/backupTrelloBoard/build.sh new file mode 100755 index 0000000..41eca98 --- /dev/null +++ b/lambdaFunctions/backupTrelloBoard/build.sh @@ -0,0 +1,3 @@ +#!/usr/bin/env bash +set -e +bundle install --path vendor/bundle diff --git a/lambdaFunctions/backupTrelloBoard/index.rb b/lambdaFunctions/backupTrelloBoard/index.rb new file mode 100644 index 0000000..db0c23c --- /dev/null +++ b/lambdaFunctions/backupTrelloBoard/index.rb @@ -0,0 +1,4 @@ +def handler(event:, context:) + p event + { statusCode: 200, body: 'OK' } +end diff --git a/lambdaFunctions/build.sh b/lambdaFunctions/build.sh index 43acb80..58e0d21 100755 --- a/lambdaFunctions/build.sh +++ b/lambdaFunctions/build.sh @@ -1,3 +1,3 @@ #!/usr/bin/env bash set -e -cd enumerateTrelloBoards && ./build.sh +find * -type d -maxdepth 0 -exec sh -c "cd {} && ./build.sh" \; From 85b71c977d811d7d9196cbdb23fe3b68b5ca20f2 Mon Sep 17 00:00:00 2001 From: James Mead <3169+floehopper@users.noreply.github.com> Date: Sat, 15 Dec 2018 14:38:37 +0000 Subject: [PATCH 21/57] Make dotenv gem available for backupTrelloBoard lambda function --- README.md | 2 ++ lambdaFunctions/backupTrelloBoard/.gitignore | 1 + lambdaFunctions/backupTrelloBoard/Gemfile | 2 ++ lambdaFunctions/backupTrelloBoard/Gemfile.lock | 2 ++ lambdaFunctions/backupTrelloBoard/index.rb | 6 ++++++ 5 files changed, 13 insertions(+) diff --git a/README.md b/README.md index 9d08846..82e675a 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,7 @@ Ensure you're logged in as the GFR Admin user. # Store the Trello API key in .env $ echo "TRELLO_KEY=$TRELLO_KEY" >> lambdaFunctions/enumerateTrelloBoards/.env + $ echo "TRELLO_KEY=$TRELLO_KEY" >> lambdaFunctions/backupTrelloBoard/.env ### Getting a Trello API token @@ -46,6 +47,7 @@ Ensure you're logged in as the GFR Admin user on https://trello.com. It's safe t # Store the Trello token in .env $ echo "TRELLO_TOKEN=$TRELLO_TOKEN" >> lambdaFunctions/enumerateTrelloBoards/.env + $ echo "TRELLO_TOKEN=$TRELLO_TOKEN" >> lambdaFunctions/backupTrelloBoard/.env ## Build diff --git a/lambdaFunctions/backupTrelloBoard/.gitignore b/lambdaFunctions/backupTrelloBoard/.gitignore index 22d0d82..226ca36 100644 --- a/lambdaFunctions/backupTrelloBoard/.gitignore +++ b/lambdaFunctions/backupTrelloBoard/.gitignore @@ -1 +1,2 @@ +.env vendor diff --git a/lambdaFunctions/backupTrelloBoard/Gemfile b/lambdaFunctions/backupTrelloBoard/Gemfile index 767e2a1..ce092d1 100644 --- a/lambdaFunctions/backupTrelloBoard/Gemfile +++ b/lambdaFunctions/backupTrelloBoard/Gemfile @@ -1,3 +1,5 @@ source 'https://rubygems.org' ruby '~> 2.5.0' + +gem 'dotenv' diff --git a/lambdaFunctions/backupTrelloBoard/Gemfile.lock b/lambdaFunctions/backupTrelloBoard/Gemfile.lock index 47c816b..b2a9648 100644 --- a/lambdaFunctions/backupTrelloBoard/Gemfile.lock +++ b/lambdaFunctions/backupTrelloBoard/Gemfile.lock @@ -1,11 +1,13 @@ GEM remote: https://rubygems.org/ specs: + dotenv (2.5.0) PLATFORMS ruby DEPENDENCIES + dotenv RUBY VERSION ruby 2.5.3p105 diff --git a/lambdaFunctions/backupTrelloBoard/index.rb b/lambdaFunctions/backupTrelloBoard/index.rb index db0c23c..3319303 100644 --- a/lambdaFunctions/backupTrelloBoard/index.rb +++ b/lambdaFunctions/backupTrelloBoard/index.rb @@ -1,4 +1,10 @@ +require 'dotenv' + +Dotenv.load + def handler(event:, context:) p event + p ENV.fetch('TRELLO_KEY') + p ENV.fetch('TRELLO_TOKEN') { statusCode: 200, body: 'OK' } end From 25701aa942f6ad4490b5428b79a03cb5b0d4c098 Mon Sep 17 00:00:00 2001 From: James Mead <3169+floehopper@users.noreply.github.com> Date: Sat, 15 Dec 2018 14:46:22 +0000 Subject: [PATCH 22/57] Download data for Trello board in backupTrelloBoard lambda function --- lambdaFunctions/backupTrelloBoard/Gemfile | 2 ++ .../backupTrelloBoard/Gemfile.lock | 20 +++++++++++ lambdaFunctions/backupTrelloBoard/index.rb | 34 +++++++++++++++++-- 3 files changed, 53 insertions(+), 3 deletions(-) diff --git a/lambdaFunctions/backupTrelloBoard/Gemfile b/lambdaFunctions/backupTrelloBoard/Gemfile index ce092d1..5fcd355 100644 --- a/lambdaFunctions/backupTrelloBoard/Gemfile +++ b/lambdaFunctions/backupTrelloBoard/Gemfile @@ -2,4 +2,6 @@ source 'https://rubygems.org' ruby '~> 2.5.0' +gem 'addressable' gem 'dotenv' +gem 'rest-client' diff --git a/lambdaFunctions/backupTrelloBoard/Gemfile.lock b/lambdaFunctions/backupTrelloBoard/Gemfile.lock index b2a9648..6eaa8e7 100644 --- a/lambdaFunctions/backupTrelloBoard/Gemfile.lock +++ b/lambdaFunctions/backupTrelloBoard/Gemfile.lock @@ -1,13 +1,33 @@ GEM remote: https://rubygems.org/ specs: + addressable (2.5.2) + public_suffix (>= 2.0.2, < 4.0) + domain_name (0.5.20180417) + unf (>= 0.0.5, < 1.0.0) dotenv (2.5.0) + http-cookie (1.0.3) + domain_name (~> 0.5) + mime-types (3.2.2) + mime-types-data (~> 3.2015) + mime-types-data (3.2018.0812) + netrc (0.11.0) + public_suffix (3.0.3) + rest-client (2.0.2) + http-cookie (>= 1.0.2, < 2.0) + mime-types (>= 1.16, < 4.0) + netrc (~> 0.8) + unf (0.1.4) + unf_ext + unf_ext (0.0.7.5) PLATFORMS ruby DEPENDENCIES + addressable dotenv + rest-client RUBY VERSION ruby 2.5.3p105 diff --git a/lambdaFunctions/backupTrelloBoard/index.rb b/lambdaFunctions/backupTrelloBoard/index.rb index 3319303..90236c5 100644 --- a/lambdaFunctions/backupTrelloBoard/index.rb +++ b/lambdaFunctions/backupTrelloBoard/index.rb @@ -1,10 +1,38 @@ +require 'addressable/uri' require 'dotenv' +require 'restclient' Dotenv.load def handler(event:, context:) - p event - p ENV.fetch('TRELLO_KEY') - p ENV.fetch('TRELLO_TOKEN') + records = event.fetch('Records', []) + sns_events = records.select { |r| r['EventSource'] == 'aws:sns' } + sns_events.each do |sns_event| + attributes = sns_event.dig('Sns', 'MessageAttributes') + board_id = attributes.dig('board_id', 'Value') + board_name = attributes.dig('board_name', 'Value') + + endpoint = "https://api.trello.com/1/boards/#{board_id}" + uri = Addressable::URI.parse(endpoint) + uri.query_values = { + :actions => :all, + :actions_limit => 1000, + :cards => :all, + :lists => :all, + :members => :all, + :member_fields => :all, + :checklists => :all, + :fields => :all, + :card_attachments => true, + :key => ENV.fetch('TRELLO_KEY'), + :token => ENV.fetch('TRELLO_TOKEN') + } + + puts "#{board_id} - Fetching Trello data for #{board_name} board..." + response = RestClient.get(uri.to_s) + json = response.body + puts "#{board_id} - OK" + end + { statusCode: 200, body: 'OK' } end From 02f10abe765718126e136d2f9a6e4c9984bc2d3d Mon Sep 17 00:00:00 2001 From: James Mead <3169+floehopper@users.noreply.github.com> Date: Sat, 15 Dec 2018 15:04:44 +0000 Subject: [PATCH 23/57] Create a versioned S3 bucket & allow backupTrelloBoard to write to it --- index.ts | 11 +++++++++++ package.json | 1 + 2 files changed, 12 insertions(+) diff --git a/index.ts b/index.ts index 46a96d3..0813d0c 100644 --- a/index.ts +++ b/index.ts @@ -1,6 +1,7 @@ import cdk = require('@aws-cdk/cdk'); import lambda = require('@aws-cdk/aws-lambda'); import events = require('@aws-cdk/aws-events'); +import s3 = require('@aws-cdk/aws-s3'); import { Topic } from '@aws-cdk/aws-sns'; class TrelloBackupStack extends cdk.Stack { @@ -19,13 +20,23 @@ class TrelloBackupStack extends cdk.Stack { timeout: 30 }); + const trelloBoardBackupsBucket = new s3.Bucket(this, 'trelloBoardBackupsBucket', { + bucketName: 'trello-board-backups.gofreerange.com', + versioned: true + }); + const backupTrelloBoard = new lambda.Function(this, 'backupTrelloBoard', { runtime: new lambda.Runtime('ruby2.5'), handler: 'index.handler', code: lambda.Code.asset('./lambdaFunctions/backupTrelloBoard'), + environment: { + BACKUP_TRELLO_BOARD_S3_BUCKET_NAME: trelloBoardBackupsBucket.bucketName + }, timeout: 30 }); + trelloBoardBackupsBucket.grantPut(backupTrelloBoard.role); + backupTrelloBoardTopic.grantPublish(enumerateTrelloBoards.role); backupTrelloBoardTopic.subscribeLambda(backupTrelloBoard); diff --git a/package.json b/package.json index e961243..221e7dc 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "dependencies": { "@aws-cdk/aws-events": "^0.20.0", "@aws-cdk/aws-lambda": "^0.20.0", + "@aws-cdk/aws-s3": "^0.20.0", "@aws-cdk/aws-sns": "^0.20.0", "aws-cdk": "^0.20.0", "typescript": "^3.2.2" From 2e6e8c957bf76f3bd51ad366650711e468f6fb59 Mon Sep 17 00:00:00 2001 From: James Mead <3169+floehopper@users.noreply.github.com> Date: Sat, 15 Dec 2018 15:05:20 +0000 Subject: [PATCH 24/57] Write Trello board data to S3 bucket I've chosen to use the board ID rather than a normalized version of the board name for the S3 object key, because this is a lot less likely to change over time. There's no need to include a timestamp in the S3 object key, because the S3 bucket has versioning enabled. --- lambdaFunctions/backupTrelloBoard/Gemfile | 1 + lambdaFunctions/backupTrelloBoard/Gemfile.lock | 17 +++++++++++++++++ lambdaFunctions/backupTrelloBoard/index.rb | 8 ++++++++ 3 files changed, 26 insertions(+) diff --git a/lambdaFunctions/backupTrelloBoard/Gemfile b/lambdaFunctions/backupTrelloBoard/Gemfile index 5fcd355..0a2f33f 100644 --- a/lambdaFunctions/backupTrelloBoard/Gemfile +++ b/lambdaFunctions/backupTrelloBoard/Gemfile @@ -3,5 +3,6 @@ source 'https://rubygems.org' ruby '~> 2.5.0' gem 'addressable' +gem 'aws-sdk-s3' gem 'dotenv' gem 'rest-client' diff --git a/lambdaFunctions/backupTrelloBoard/Gemfile.lock b/lambdaFunctions/backupTrelloBoard/Gemfile.lock index 6eaa8e7..2d335ca 100644 --- a/lambdaFunctions/backupTrelloBoard/Gemfile.lock +++ b/lambdaFunctions/backupTrelloBoard/Gemfile.lock @@ -3,11 +3,27 @@ GEM specs: addressable (2.5.2) public_suffix (>= 2.0.2, < 4.0) + aws-eventstream (1.0.1) + aws-partitions (1.125.0) + aws-sdk-core (3.44.0) + aws-eventstream (~> 1.0) + aws-partitions (~> 1.0) + aws-sigv4 (~> 1.0) + jmespath (~> 1.0) + aws-sdk-kms (1.13.0) + aws-sdk-core (~> 3, >= 3.39.0) + aws-sigv4 (~> 1.0) + aws-sdk-s3 (1.30.0) + aws-sdk-core (~> 3, >= 3.39.0) + aws-sdk-kms (~> 1) + aws-sigv4 (~> 1.0) + aws-sigv4 (1.0.3) domain_name (0.5.20180417) unf (>= 0.0.5, < 1.0.0) dotenv (2.5.0) http-cookie (1.0.3) domain_name (~> 0.5) + jmespath (1.4.0) mime-types (3.2.2) mime-types-data (~> 3.2015) mime-types-data (3.2018.0812) @@ -26,6 +42,7 @@ PLATFORMS DEPENDENCIES addressable + aws-sdk-s3 dotenv rest-client diff --git a/lambdaFunctions/backupTrelloBoard/index.rb b/lambdaFunctions/backupTrelloBoard/index.rb index 90236c5..6ec8879 100644 --- a/lambdaFunctions/backupTrelloBoard/index.rb +++ b/lambdaFunctions/backupTrelloBoard/index.rb @@ -1,9 +1,12 @@ require 'addressable/uri' +require 'aws-sdk-s3' require 'dotenv' require 'restclient' Dotenv.load +S3_BUCKET_NAME = ENV.fetch('BACKUP_TRELLO_BOARD_S3_BUCKET_NAME') + def handler(event:, context:) records = event.fetch('Records', []) sns_events = records.select { |r| r['EventSource'] == 'aws:sns' } @@ -32,6 +35,11 @@ def handler(event:, context:) response = RestClient.get(uri.to_s) json = response.body puts "#{board_id} - OK" + + puts "#{board_id} - Writing data for #{board_name} board to S3..." + object = Aws::S3::Object.new(S3_BUCKET_NAME, "#{board_id}.json") + object.upload_stream(content_type: 'application/json') { |stream| stream << json } + puts "#{board_id} - OK" end { statusCode: 200, body: 'OK' } From 03b02ead2483d21f5ae493b4ca3861e8d42b23d2 Mon Sep 17 00:00:00 2001 From: James Mead <3169+floehopper@users.noreply.github.com> Date: Sun, 23 Dec 2018 13:49:44 +0000 Subject: [PATCH 25/57] Rename local constants which reference Lambda functions --- index.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/index.ts b/index.ts index 0813d0c..40cd659 100644 --- a/index.ts +++ b/index.ts @@ -10,7 +10,7 @@ class TrelloBackupStack extends cdk.Stack { const backupTrelloBoardTopic = new Topic(this, 'backupTrelloBoardTopic'); - const enumerateTrelloBoards = new lambda.Function(this, 'enumerateTrelloBoards', { + const enumerateTrelloBoardsFunction = new lambda.Function(this, 'enumerateTrelloBoards', { runtime: new lambda.Runtime('ruby2.5'), handler: 'index.handler', code: lambda.Code.asset('./lambdaFunctions/enumerateTrelloBoards'), @@ -25,7 +25,7 @@ class TrelloBackupStack extends cdk.Stack { versioned: true }); - const backupTrelloBoard = new lambda.Function(this, 'backupTrelloBoard', { + const backupTrelloBoardFunction = new lambda.Function(this, 'backupTrelloBoard', { runtime: new lambda.Runtime('ruby2.5'), handler: 'index.handler', code: lambda.Code.asset('./lambdaFunctions/backupTrelloBoard'), @@ -35,16 +35,16 @@ class TrelloBackupStack extends cdk.Stack { timeout: 30 }); - trelloBoardBackupsBucket.grantPut(backupTrelloBoard.role); + trelloBoardBackupsBucket.grantPut(backupTrelloBoardFunction.role); - backupTrelloBoardTopic.grantPublish(enumerateTrelloBoards.role); - backupTrelloBoardTopic.subscribeLambda(backupTrelloBoard); + backupTrelloBoardTopic.grantPublish(enumerateTrelloBoardsFunction.role); + backupTrelloBoardTopic.subscribeLambda(backupTrelloBoardFunction); const dailyAt2am = 'cron(0 2 * * ? *)'; const dailyAt2amRule = new events.EventRule(this, 'DailyAt2amRule', { scheduleExpression: dailyAt2am, }); - dailyAt2amRule.addTarget(enumerateTrelloBoards); + dailyAt2amRule.addTarget(enumerateTrelloBoardsFunction); } } From 8b30f73273fd3b3fc3810f3e171622cce0112cd6 Mon Sep 17 00:00:00 2001 From: James Mead <3169+floehopper@users.noreply.github.com> Date: Sun, 23 Dec 2018 14:05:40 +0000 Subject: [PATCH 26/57] Extract creating AWS resources into functions --- index.ts | 43 +++++++++++++++++++++++++++++-------------- 1 file changed, 29 insertions(+), 14 deletions(-) diff --git a/index.ts b/index.ts index 40cd659..b4ebf8a 100644 --- a/index.ts +++ b/index.ts @@ -10,7 +10,29 @@ class TrelloBackupStack extends cdk.Stack { const backupTrelloBoardTopic = new Topic(this, 'backupTrelloBoardTopic'); - const enumerateTrelloBoardsFunction = new lambda.Function(this, 'enumerateTrelloBoards', { + const enumerateTrelloBoardsFunction + = this.createEnumerateTrelloBoardsFunction(backupTrelloBoardTopic); + + const trelloBoardBackupsBucket + = this.createTrelloBoardBackupsBucket(); + + const backupTrelloBoardFunction + = this.createBackupTrelloBoardFunction(trelloBoardBackupsBucket); + + trelloBoardBackupsBucket.grantPut(backupTrelloBoardFunction.role); + + backupTrelloBoardTopic.grantPublish(enumerateTrelloBoardsFunction.role); + backupTrelloBoardTopic.subscribeLambda(backupTrelloBoardFunction); + + const dailyAt2am = 'cron(0 2 * * ? *)'; + const dailyAt2amRule = new events.EventRule(this, 'DailyAt2amRule', { + scheduleExpression: dailyAt2am, + }); + dailyAt2amRule.addTarget(enumerateTrelloBoardsFunction); + } + + createEnumerateTrelloBoardsFunction(backupTrelloBoardTopic : Topic) : lambda.Function { + return new lambda.Function(this, 'enumerateTrelloBoards', { runtime: new lambda.Runtime('ruby2.5'), handler: 'index.handler', code: lambda.Code.asset('./lambdaFunctions/enumerateTrelloBoards'), @@ -19,13 +41,17 @@ class TrelloBackupStack extends cdk.Stack { }, timeout: 30 }); + } - const trelloBoardBackupsBucket = new s3.Bucket(this, 'trelloBoardBackupsBucket', { + createTrelloBoardBackupsBucket() : s3.Bucket { + return new s3.Bucket(this, 'trelloBoardBackupsBucket', { bucketName: 'trello-board-backups.gofreerange.com', versioned: true }); + } - const backupTrelloBoardFunction = new lambda.Function(this, 'backupTrelloBoard', { + createBackupTrelloBoardFunction(trelloBoardBackupsBucket : s3.Bucket) : lambda.Function { + return new lambda.Function(this, 'backupTrelloBoard', { runtime: new lambda.Runtime('ruby2.5'), handler: 'index.handler', code: lambda.Code.asset('./lambdaFunctions/backupTrelloBoard'), @@ -34,17 +60,6 @@ class TrelloBackupStack extends cdk.Stack { }, timeout: 30 }); - - trelloBoardBackupsBucket.grantPut(backupTrelloBoardFunction.role); - - backupTrelloBoardTopic.grantPublish(enumerateTrelloBoardsFunction.role); - backupTrelloBoardTopic.subscribeLambda(backupTrelloBoardFunction); - - const dailyAt2am = 'cron(0 2 * * ? *)'; - const dailyAt2amRule = new events.EventRule(this, 'DailyAt2amRule', { - scheduleExpression: dailyAt2am, - }); - dailyAt2amRule.addTarget(enumerateTrelloBoardsFunction); } } From 36a3f961669ff4af913cbaa45cc7319757670cc8 Mon Sep 17 00:00:00 2001 From: James Mead <3169+floehopper@users.noreply.github.com> Date: Sun, 23 Dec 2018 14:12:35 +0000 Subject: [PATCH 27/57] Move granting of permissions into create functions --- index.ts | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/index.ts b/index.ts index b4ebf8a..22c2bd4 100644 --- a/index.ts +++ b/index.ts @@ -19,9 +19,6 @@ class TrelloBackupStack extends cdk.Stack { const backupTrelloBoardFunction = this.createBackupTrelloBoardFunction(trelloBoardBackupsBucket); - trelloBoardBackupsBucket.grantPut(backupTrelloBoardFunction.role); - - backupTrelloBoardTopic.grantPublish(enumerateTrelloBoardsFunction.role); backupTrelloBoardTopic.subscribeLambda(backupTrelloBoardFunction); const dailyAt2am = 'cron(0 2 * * ? *)'; @@ -32,7 +29,7 @@ class TrelloBackupStack extends cdk.Stack { } createEnumerateTrelloBoardsFunction(backupTrelloBoardTopic : Topic) : lambda.Function { - return new lambda.Function(this, 'enumerateTrelloBoards', { + const lambdaFunction = new lambda.Function(this, 'enumerateTrelloBoards', { runtime: new lambda.Runtime('ruby2.5'), handler: 'index.handler', code: lambda.Code.asset('./lambdaFunctions/enumerateTrelloBoards'), @@ -41,6 +38,8 @@ class TrelloBackupStack extends cdk.Stack { }, timeout: 30 }); + backupTrelloBoardTopic.grantPublish(lambdaFunction.role); + return lambdaFunction; } createTrelloBoardBackupsBucket() : s3.Bucket { @@ -51,7 +50,7 @@ class TrelloBackupStack extends cdk.Stack { } createBackupTrelloBoardFunction(trelloBoardBackupsBucket : s3.Bucket) : lambda.Function { - return new lambda.Function(this, 'backupTrelloBoard', { + const lambdaFunction = new lambda.Function(this, 'backupTrelloBoard', { runtime: new lambda.Runtime('ruby2.5'), handler: 'index.handler', code: lambda.Code.asset('./lambdaFunctions/backupTrelloBoard'), @@ -60,6 +59,8 @@ class TrelloBackupStack extends cdk.Stack { }, timeout: 30 }); + trelloBoardBackupsBucket.grantPut(lambdaFunction.role); + return lambdaFunction; } } From 7bee932dc71ceacb4fc3e26015cc708603cfd23b Mon Sep 17 00:00:00 2001 From: James Mead <3169+floehopper@users.noreply.github.com> Date: Sun, 23 Dec 2018 14:36:32 +0000 Subject: [PATCH 28/57] Extract function to schedule lamda function invocation --- index.ts | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/index.ts b/index.ts index 22c2bd4..34e5b55 100644 --- a/index.ts +++ b/index.ts @@ -21,11 +21,7 @@ class TrelloBackupStack extends cdk.Stack { backupTrelloBoardTopic.subscribeLambda(backupTrelloBoardFunction); - const dailyAt2am = 'cron(0 2 * * ? *)'; - const dailyAt2amRule = new events.EventRule(this, 'DailyAt2amRule', { - scheduleExpression: dailyAt2am, - }); - dailyAt2amRule.addTarget(enumerateTrelloBoardsFunction); + this.scheduleDailyAt2am(enumerateTrelloBoardsFunction); } createEnumerateTrelloBoardsFunction(backupTrelloBoardTopic : Topic) : lambda.Function { @@ -62,6 +58,14 @@ class TrelloBackupStack extends cdk.Stack { trelloBoardBackupsBucket.grantPut(lambdaFunction.role); return lambdaFunction; } + + scheduleDailyAt2am(lambdaFunction : lambda.Function) : void { + const dailyAt2am = 'cron(0 2 * * ? *)'; + const dailyAt2amRule = new events.EventRule(this, 'DailyAt2amRule', { + scheduleExpression: dailyAt2am, + }); + dailyAt2amRule.addTarget(lambdaFunction); + } } const app = new cdk.App(); From 967a288a8838330c1a83b8e9d3e9b0cb5228456b Mon Sep 17 00:00:00 2001 From: James Mead <3169+floehopper@users.noreply.github.com> Date: Sun, 23 Dec 2018 17:12:43 +0000 Subject: [PATCH 29/57] Allow environment variables to be set from .env files --- .gitignore | 1 + index.ts | 3 +++ package-lock.json | 18 ++++++++++++++++++ package.json | 2 ++ 4 files changed, 24 insertions(+) diff --git a/.gitignore b/.gitignore index 012a3cd..fec2f30 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ index.js +.env diff --git a/index.ts b/index.ts index 34e5b55..b3be6ff 100644 --- a/index.ts +++ b/index.ts @@ -3,6 +3,9 @@ import lambda = require('@aws-cdk/aws-lambda'); import events = require('@aws-cdk/aws-events'); import s3 = require('@aws-cdk/aws-s3'); import { Topic } from '@aws-cdk/aws-sns'; +import dotenv = require('dotenv'); + +dotenv.config(); class TrelloBackupStack extends cdk.Stack { constructor(parent: cdk.App, id: string, props?: cdk.StackProps) { diff --git a/package-lock.json b/package-lock.json index 2a21a69..0fec457 100644 --- a/package-lock.json +++ b/package-lock.json @@ -270,6 +270,19 @@ "resolved": "https://registry.npmjs.org/@aws-cdk/cx-api/-/cx-api-0.20.0.tgz", "integrity": "sha512-tbV4msow2+V9xUIDiYzD3ZGBKkdOzMYicR7zXl98i5jJT3EphbMDzulsJ7LVEMKyFIV/1q0NA0k3SJ6T22nVSw==" }, + "@types/dotenv": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@types/dotenv/-/dotenv-6.1.0.tgz", + "integrity": "sha512-gmbNb7V1LbJQA4MmH0hVFgqY1cyKsa6RvKC1Xrq0WBnZ0JuuvXKciXx/s8dN0LVXCJd8xO6wIaSFSyUIoGph9g==", + "requires": { + "@types/node": "*" + } + }, + "@types/node": { + "version": "10.12.18", + "resolved": "https://registry.npmjs.org/@types/node/-/node-10.12.18.tgz", + "integrity": "sha512-fh+pAqt4xRzPfqA6eh3Z2y6fyZavRIumvjhaCL753+TVkGKGhpPeyrJG2JftD0T9q4GF00KjefsQ+PQNDdWQaQ==" + }, "agent-base": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-4.2.1.tgz", @@ -752,6 +765,11 @@ "heap": ">= 0.2.0" } }, + "dotenv": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-6.2.0.tgz", + "integrity": "sha512-HygQCKUBSFl8wKQZBSemMywRWcEDNidvNbjGVyZu3nbZ8qq9ubiPoGLMdRDpfSrpkkm9BXYFkpKxxFX38o/76w==" + }, "dreamopt": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/dreamopt/-/dreamopt-0.6.0.tgz", diff --git a/package.json b/package.json index 221e7dc..9dccf3f 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,9 @@ "@aws-cdk/aws-lambda": "^0.20.0", "@aws-cdk/aws-s3": "^0.20.0", "@aws-cdk/aws-sns": "^0.20.0", + "@types/dotenv": "^6.1.0", "aws-cdk": "^0.20.0", + "dotenv": "^6.2.0", "typescript": "^3.2.2" } } From 8d0ded53338c7652afed016b2cd8033fbc8a6e85 Mon Sep 17 00:00:00 2001 From: James Mead <3169+floehopper@users.noreply.github.com> Date: Sun, 23 Dec 2018 17:17:47 +0000 Subject: [PATCH 30/57] Use env var to specify S3 bucket name for backups --- README.md | 4 ++++ index.ts | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 82e675a..f404924 100644 --- a/README.md +++ b/README.md @@ -49,6 +49,10 @@ Ensure you're logged in as the GFR Admin user on https://trello.com. It's safe t $ echo "TRELLO_TOKEN=$TRELLO_TOKEN" >> lambdaFunctions/enumerateTrelloBoards/.env $ echo "TRELLO_TOKEN=$TRELLO_TOKEN" >> lambdaFunctions/backupTrelloBoard/.env +### Setting S3 bucket name + +In the top-level `.env` file, set the `TRELLO_BOARD_BACKUPS_BUCKET_NAME` environment variable to the name of the S3 bucket you want to be created and used to save backups. + ## Build npm run build diff --git a/index.ts b/index.ts index b3be6ff..8b10aea 100644 --- a/index.ts +++ b/index.ts @@ -43,7 +43,7 @@ class TrelloBackupStack extends cdk.Stack { createTrelloBoardBackupsBucket() : s3.Bucket { return new s3.Bucket(this, 'trelloBoardBackupsBucket', { - bucketName: 'trello-board-backups.gofreerange.com', + bucketName: process.env.TRELLO_BOARD_BACKUPS_BUCKET_NAME, versioned: true }); } From 4ec16c23630ec223fd7710d3d4b268d5caf72183 Mon Sep 17 00:00:00 2001 From: James Mead <3169+floehopper@users.noreply.github.com> Date: Sun, 23 Dec 2018 17:32:39 +0000 Subject: [PATCH 31/57] Email notification for errors in lamda functions I've confirmed that even a single exception from one of the lambda functions will trigger an alarm email notification. --- README.md | 7 +++++-- index.ts | 28 ++++++++++++++++++++++++---- 2 files changed, 29 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index f404924..afda5b7 100644 --- a/README.md +++ b/README.md @@ -49,9 +49,12 @@ Ensure you're logged in as the GFR Admin user on https://trello.com. It's safe t $ echo "TRELLO_TOKEN=$TRELLO_TOKEN" >> lambdaFunctions/enumerateTrelloBoards/.env $ echo "TRELLO_TOKEN=$TRELLO_TOKEN" >> lambdaFunctions/backupTrelloBoard/.env -### Setting S3 bucket name +### Environment variables -In the top-level `.env` file, set the `TRELLO_BOARD_BACKUPS_BUCKET_NAME` environment variable to the name of the S3 bucket you want to be created and used to save backups. +In the top-level `.env` file, set the following environment variables: + +* `TRELLO_BOARD_BACKUPS_BUCKET_NAME` - the name of the S3 bucket you want to be created and used to save backups +* `TRELLO_BOARD_BACKUPS_ALARM_EMAIL_ADDRESS` - the email address where alarm emails will be sent (an email will be sent on deployment to ask you to confirm the subscription) ## Build diff --git a/index.ts b/index.ts index 8b10aea..b0f4666 100644 --- a/index.ts +++ b/index.ts @@ -11,23 +11,27 @@ class TrelloBackupStack extends cdk.Stack { constructor(parent: cdk.App, id: string, props?: cdk.StackProps) { super(parent, id, props); + const alarmTopic = new Topic(this, 'alarmTopic'); const backupTrelloBoardTopic = new Topic(this, 'backupTrelloBoardTopic'); const enumerateTrelloBoardsFunction - = this.createEnumerateTrelloBoardsFunction(backupTrelloBoardTopic); + = this.createEnumerateTrelloBoardsFunction(backupTrelloBoardTopic, alarmTopic); const trelloBoardBackupsBucket = this.createTrelloBoardBackupsBucket(); const backupTrelloBoardFunction - = this.createBackupTrelloBoardFunction(trelloBoardBackupsBucket); + = this.createBackupTrelloBoardFunction(trelloBoardBackupsBucket, alarmTopic); backupTrelloBoardTopic.subscribeLambda(backupTrelloBoardFunction); + const alarmEmailAddress = process.env.TRELLO_BOARD_BACKUPS_ALARM_EMAIL_ADDRESS; + alarmTopic.subscribeEmail('alarmTopicEmail', alarmEmailAddress); + this.scheduleDailyAt2am(enumerateTrelloBoardsFunction); } - createEnumerateTrelloBoardsFunction(backupTrelloBoardTopic : Topic) : lambda.Function { + createEnumerateTrelloBoardsFunction(backupTrelloBoardTopic : Topic, alarmTopic : Topic) : lambda.Function { const lambdaFunction = new lambda.Function(this, 'enumerateTrelloBoards', { runtime: new lambda.Runtime('ruby2.5'), handler: 'index.handler', @@ -38,6 +42,14 @@ class TrelloBackupStack extends cdk.Stack { timeout: 30 }); backupTrelloBoardTopic.grantPublish(lambdaFunction.role); + + const metricErrors = lambdaFunction.metricErrors(); + const alarm = metricErrors.newAlarm(this, 'enumerateTrelloBoardsAlarm', { + threshold: 1, + evaluationPeriods: 1 + }); + alarm.onAlarm(alarmTopic); + return lambdaFunction; } @@ -48,7 +60,7 @@ class TrelloBackupStack extends cdk.Stack { }); } - createBackupTrelloBoardFunction(trelloBoardBackupsBucket : s3.Bucket) : lambda.Function { + createBackupTrelloBoardFunction(trelloBoardBackupsBucket : s3.Bucket, alarmTopic : Topic) : lambda.Function { const lambdaFunction = new lambda.Function(this, 'backupTrelloBoard', { runtime: new lambda.Runtime('ruby2.5'), handler: 'index.handler', @@ -59,6 +71,14 @@ class TrelloBackupStack extends cdk.Stack { timeout: 30 }); trelloBoardBackupsBucket.grantPut(lambdaFunction.role); + + const metricErrors = lambdaFunction.metricErrors(); + const alarm = metricErrors.newAlarm(this, 'backupTrelloBoardAlarm', { + threshold: 1, + evaluationPeriods: 1 + }); + alarm.onAlarm(alarmTopic); + return lambdaFunction; } From ab877f461164469654d757f914fd89317f94ef02 Mon Sep 17 00:00:00 2001 From: James Mead <3169+floehopper@users.noreply.github.com> Date: Sun, 23 Dec 2018 17:57:55 +0000 Subject: [PATCH 32/57] Extract reportErrors method to reduce duplication --- index.ts | 27 +++++++++++---------------- 1 file changed, 11 insertions(+), 16 deletions(-) diff --git a/index.ts b/index.ts index b0f4666..44d4ec0 100644 --- a/index.ts +++ b/index.ts @@ -42,14 +42,7 @@ class TrelloBackupStack extends cdk.Stack { timeout: 30 }); backupTrelloBoardTopic.grantPublish(lambdaFunction.role); - - const metricErrors = lambdaFunction.metricErrors(); - const alarm = metricErrors.newAlarm(this, 'enumerateTrelloBoardsAlarm', { - threshold: 1, - evaluationPeriods: 1 - }); - alarm.onAlarm(alarmTopic); - + this.reportErrors(lambdaFunction, alarmTopic); return lambdaFunction; } @@ -71,14 +64,7 @@ class TrelloBackupStack extends cdk.Stack { timeout: 30 }); trelloBoardBackupsBucket.grantPut(lambdaFunction.role); - - const metricErrors = lambdaFunction.metricErrors(); - const alarm = metricErrors.newAlarm(this, 'backupTrelloBoardAlarm', { - threshold: 1, - evaluationPeriods: 1 - }); - alarm.onAlarm(alarmTopic); - + this.reportErrors(lambdaFunction, alarmTopic); return lambdaFunction; } @@ -89,6 +75,15 @@ class TrelloBackupStack extends cdk.Stack { }); dailyAt2amRule.addTarget(lambdaFunction); } + + reportErrors(lambdaFunction : lambda.Function, alarmTopic : Topic) : void { + const metricErrors = lambdaFunction.metricErrors(); + const alarm = metricErrors.newAlarm(this, `${lambdaFunction.id}Alarm`, { + threshold: 1, + evaluationPeriods: 1 + }); + alarm.onAlarm(alarmTopic); + } } const app = new cdk.App(); From 9705993cca5a701e72f80521b0fd498f7c629e37 Mon Sep 17 00:00:00 2001 From: James Mead <3169+floehopper@users.noreply.github.com> Date: Sun, 23 Dec 2018 18:08:41 +0000 Subject: [PATCH 33/57] Extract schedule expression into env var --- README.md | 2 ++ index.ts | 12 ++++++------ 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index afda5b7..7de3332 100644 --- a/README.md +++ b/README.md @@ -53,6 +53,7 @@ Ensure you're logged in as the GFR Admin user on https://trello.com. It's safe t In the top-level `.env` file, set the following environment variables: +* `TRELLO_BOARD_BACKUPS_SCHEDULE_EXPRESSION` - specifies how often the backup is performed, e.g. "cron(0 2 * * ? *)" runs daily at 2am (see [Schedule Expressions for Rules][3] for details) * `TRELLO_BOARD_BACKUPS_BUCKET_NAME` - the name of the S3 bucket you want to be created and used to save backups * `TRELLO_BOARD_BACKUPS_ALARM_EMAIL_ADDRESS` - the email address where alarm emails will be sent (an email will be sent on deployment to ask you to confirm the subscription) @@ -79,3 +80,4 @@ the first time, the environment must be bootstrapped using: `cdk bootstrap`. [1]: https://awslabs.github.io/aws-cdk/getting-started.html#configuring-the-cdk-toolkit [2]: https://awslabs.github.io/aws-cdk/tools.html#command-line-toolkit-cdk +[3]: https://docs.aws.amazon.com/AmazonCloudWatch/latest/events/ScheduledEvents.html diff --git a/index.ts b/index.ts index 44d4ec0..3d687a0 100644 --- a/index.ts +++ b/index.ts @@ -28,7 +28,8 @@ class TrelloBackupStack extends cdk.Stack { const alarmEmailAddress = process.env.TRELLO_BOARD_BACKUPS_ALARM_EMAIL_ADDRESS; alarmTopic.subscribeEmail('alarmTopicEmail', alarmEmailAddress); - this.scheduleDailyAt2am(enumerateTrelloBoardsFunction); + const scheduleExpression = process.env.TRELLO_BOARD_BACKUPS_SCHEDULE_EXPRESSION; + this.schedule(enumerateTrelloBoardsFunction, scheduleExpression); } createEnumerateTrelloBoardsFunction(backupTrelloBoardTopic : Topic, alarmTopic : Topic) : lambda.Function { @@ -68,12 +69,11 @@ class TrelloBackupStack extends cdk.Stack { return lambdaFunction; } - scheduleDailyAt2am(lambdaFunction : lambda.Function) : void { - const dailyAt2am = 'cron(0 2 * * ? *)'; - const dailyAt2amRule = new events.EventRule(this, 'DailyAt2amRule', { - scheduleExpression: dailyAt2am, + schedule(lambdaFunction : lambda.Function, scheduleExpression : string) : void { + const rule = new events.EventRule(this, 'ScheduleExpressionRule', { + scheduleExpression: scheduleExpression, }); - dailyAt2amRule.addTarget(lambdaFunction); + rule.addTarget(lambdaFunction); } reportErrors(lambdaFunction : lambda.Function, alarmTopic : Topic) : void { From 19e925798bdf577c85681a4b5b58e43b8f2ad214 Mon Sep 17 00:00:00 2001 From: James Mead <3169+floehopper@users.noreply.github.com> Date: Sun, 23 Dec 2018 18:10:55 +0000 Subject: [PATCH 34/57] Move bucket name env var up into constructor This means all the env vars are obtained at the same level. --- index.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/index.ts b/index.ts index 3d687a0..795df4b 100644 --- a/index.ts +++ b/index.ts @@ -17,8 +17,9 @@ class TrelloBackupStack extends cdk.Stack { const enumerateTrelloBoardsFunction = this.createEnumerateTrelloBoardsFunction(backupTrelloBoardTopic, alarmTopic); + const bucketName = process.env.TRELLO_BOARD_BACKUPS_BUCKET_NAME; const trelloBoardBackupsBucket - = this.createTrelloBoardBackupsBucket(); + = this.createTrelloBoardBackupsBucket(bucketName); const backupTrelloBoardFunction = this.createBackupTrelloBoardFunction(trelloBoardBackupsBucket, alarmTopic); @@ -47,9 +48,9 @@ class TrelloBackupStack extends cdk.Stack { return lambdaFunction; } - createTrelloBoardBackupsBucket() : s3.Bucket { + createTrelloBoardBackupsBucket(bucketName : string) : s3.Bucket { return new s3.Bucket(this, 'trelloBoardBackupsBucket', { - bucketName: process.env.TRELLO_BOARD_BACKUPS_BUCKET_NAME, + bucketName: bucketName, versioned: true }); } From ae0e4316c7544a7555c8f5683ec823019252d15d Mon Sep 17 00:00:00 2001 From: James Mead <3169+floehopper@users.noreply.github.com> Date: Sun, 23 Dec 2018 18:15:35 +0000 Subject: [PATCH 35/57] Extract Ruby Lambda runtime into constant to reduce duplication --- index.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/index.ts b/index.ts index 795df4b..fa69733 100644 --- a/index.ts +++ b/index.ts @@ -7,6 +7,8 @@ import dotenv = require('dotenv'); dotenv.config(); +const rubyLambdaRuntime = new lambda.Runtime('ruby2.5'); + class TrelloBackupStack extends cdk.Stack { constructor(parent: cdk.App, id: string, props?: cdk.StackProps) { super(parent, id, props); @@ -35,7 +37,7 @@ class TrelloBackupStack extends cdk.Stack { createEnumerateTrelloBoardsFunction(backupTrelloBoardTopic : Topic, alarmTopic : Topic) : lambda.Function { const lambdaFunction = new lambda.Function(this, 'enumerateTrelloBoards', { - runtime: new lambda.Runtime('ruby2.5'), + runtime: rubyLambdaRuntime, handler: 'index.handler', code: lambda.Code.asset('./lambdaFunctions/enumerateTrelloBoards'), environment: { @@ -57,7 +59,7 @@ class TrelloBackupStack extends cdk.Stack { createBackupTrelloBoardFunction(trelloBoardBackupsBucket : s3.Bucket, alarmTopic : Topic) : lambda.Function { const lambdaFunction = new lambda.Function(this, 'backupTrelloBoard', { - runtime: new lambda.Runtime('ruby2.5'), + runtime: rubyLambdaRuntime, handler: 'index.handler', code: lambda.Code.asset('./lambdaFunctions/backupTrelloBoard'), environment: { From b1ca0555479db8a4a1488868a1f0321b3d20959b Mon Sep 17 00:00:00 2001 From: James Mead <3169+floehopper@users.noreply.github.com> Date: Sun, 23 Dec 2018 18:17:06 +0000 Subject: [PATCH 36/57] Extract lambda function timeout into constant to reduce duplication --- index.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/index.ts b/index.ts index fa69733..af8f1e5 100644 --- a/index.ts +++ b/index.ts @@ -8,6 +8,7 @@ import dotenv = require('dotenv'); dotenv.config(); const rubyLambdaRuntime = new lambda.Runtime('ruby2.5'); +const lambdaFunctionTimeout = 30; class TrelloBackupStack extends cdk.Stack { constructor(parent: cdk.App, id: string, props?: cdk.StackProps) { @@ -43,7 +44,7 @@ class TrelloBackupStack extends cdk.Stack { environment: { BACKUP_TRELLO_BOARD_TOPIC_ARN: backupTrelloBoardTopic.topicArn }, - timeout: 30 + timeout: lambdaFunctionTimeout }); backupTrelloBoardTopic.grantPublish(lambdaFunction.role); this.reportErrors(lambdaFunction, alarmTopic); @@ -65,7 +66,7 @@ class TrelloBackupStack extends cdk.Stack { environment: { BACKUP_TRELLO_BOARD_S3_BUCKET_NAME: trelloBoardBackupsBucket.bucketName }, - timeout: 30 + timeout: lambdaFunctionTimeout }); trelloBoardBackupsBucket.grantPut(lambdaFunction.role); this.reportErrors(lambdaFunction, alarmTopic); From 34f7e659a98f560f156eb6edcfa7a1d785dbe5bb Mon Sep 17 00:00:00 2001 From: James Mead <3169+floehopper@users.noreply.github.com> Date: Sun, 23 Dec 2018 21:22:04 +0000 Subject: [PATCH 37/57] Install bundled gems from scratch To minimize the size of the zip file uploaded to AWS. --- lambdaFunctions/backupTrelloBoard/build.sh | 1 + lambdaFunctions/enumerateTrelloBoards/build.sh | 1 + 2 files changed, 2 insertions(+) diff --git a/lambdaFunctions/backupTrelloBoard/build.sh b/lambdaFunctions/backupTrelloBoard/build.sh index 41eca98..8edff89 100755 --- a/lambdaFunctions/backupTrelloBoard/build.sh +++ b/lambdaFunctions/backupTrelloBoard/build.sh @@ -1,3 +1,4 @@ #!/usr/bin/env bash set -e +rm -rf vendor/bundle bundle install --path vendor/bundle diff --git a/lambdaFunctions/enumerateTrelloBoards/build.sh b/lambdaFunctions/enumerateTrelloBoards/build.sh index 41eca98..8edff89 100755 --- a/lambdaFunctions/enumerateTrelloBoards/build.sh +++ b/lambdaFunctions/enumerateTrelloBoards/build.sh @@ -1,3 +1,4 @@ #!/usr/bin/env bash set -e +rm -rf vendor/bundle bundle install --path vendor/bundle From 9693ccd070cd5fec308432ed840d23f6adb2a0bb Mon Sep 17 00:00:00 2001 From: James Mead <3169+floehopper@users.noreply.github.com> Date: Sun, 23 Dec 2018 22:11:23 +0000 Subject: [PATCH 38/57] Avoid using ruby-trello gem to reduce dependencies And reduce the size of the zip file uploaded to AWS. --- lambdaFunctions/enumerateTrelloBoards/Gemfile | 3 ++- .../enumerateTrelloBoards/Gemfile.lock | 25 ++--------------- .../enumerateTrelloBoards/index.rb | 27 ++++++++++++------- 3 files changed, 21 insertions(+), 34 deletions(-) diff --git a/lambdaFunctions/enumerateTrelloBoards/Gemfile b/lambdaFunctions/enumerateTrelloBoards/Gemfile index 2123d97..5a6de57 100644 --- a/lambdaFunctions/enumerateTrelloBoards/Gemfile +++ b/lambdaFunctions/enumerateTrelloBoards/Gemfile @@ -2,6 +2,7 @@ source 'https://rubygems.org' ruby '~> 2.5.0' +gem 'addressable' gem 'aws-sdk-sns' gem 'dotenv' -gem 'ruby-trello' +gem 'rest-client' diff --git a/lambdaFunctions/enumerateTrelloBoards/Gemfile.lock b/lambdaFunctions/enumerateTrelloBoards/Gemfile.lock index 3e12e4d..1126abf 100644 --- a/lambdaFunctions/enumerateTrelloBoards/Gemfile.lock +++ b/lambdaFunctions/enumerateTrelloBoards/Gemfile.lock @@ -1,13 +1,6 @@ GEM remote: https://rubygems.org/ specs: - activemodel (5.2.2) - activesupport (= 5.2.2) - activesupport (5.2.2) - concurrent-ruby (~> 1.0, >= 1.0.2) - i18n (>= 0.7, < 2) - minitest (~> 5.1) - tzinfo (~> 1.1) addressable (2.5.2) public_suffix (>= 2.0.2, < 4.0) aws-eventstream (1.0.1) @@ -21,36 +14,21 @@ GEM aws-sdk-core (~> 3, >= 3.39.0) aws-sigv4 (~> 1.0) aws-sigv4 (1.0.3) - concurrent-ruby (1.1.3) domain_name (0.5.20180417) unf (>= 0.0.5, < 1.0.0) dotenv (2.5.0) http-cookie (1.0.3) domain_name (~> 0.5) - i18n (1.2.0) - concurrent-ruby (~> 1.0) jmespath (1.4.0) - json (2.1.0) mime-types (3.2.2) mime-types-data (~> 3.2015) mime-types-data (3.2018.0812) - minitest (5.11.3) netrc (0.11.0) - oauth (0.5.4) public_suffix (3.0.3) rest-client (2.0.2) http-cookie (>= 1.0.2, < 2.0) mime-types (>= 1.16, < 4.0) netrc (~> 0.8) - ruby-trello (2.1.0) - activemodel (>= 3.2.0) - addressable (~> 2.3) - json - oauth (>= 0.4.5) - rest-client (>= 1.8.0) - thread_safe (0.3.6) - tzinfo (1.2.5) - thread_safe (~> 0.1) unf (0.1.4) unf_ext unf_ext (0.0.7.5) @@ -59,9 +37,10 @@ PLATFORMS ruby DEPENDENCIES + addressable aws-sdk-sns dotenv - ruby-trello + rest-client RUBY VERSION ruby 2.5.3p105 diff --git a/lambdaFunctions/enumerateTrelloBoards/index.rb b/lambdaFunctions/enumerateTrelloBoards/index.rb index e030939..0cfbd62 100644 --- a/lambdaFunctions/enumerateTrelloBoards/index.rb +++ b/lambdaFunctions/enumerateTrelloBoards/index.rb @@ -1,32 +1,39 @@ +require 'addressable/uri' require 'aws-sdk-sns' require 'dotenv' -require 'trello' +require 'restclient' Dotenv.load -Trello.configure do |config| - config.developer_public_key = ENV.fetch('TRELLO_KEY') - config.member_token = ENV.fetch('TRELLO_TOKEN') -end - TOPIC_ARN = ENV.fetch('BACKUP_TRELLO_BOARD_TOPIC_ARN') def handler(event:, context:) sns = Aws::SNS::Client.new - boards = Trello::Board.all + + endpoint = "https://api.trello.com/1/members/me/boards" + uri = Addressable::URI.parse(endpoint) + uri.query_values = { + :key => ENV.fetch('TRELLO_KEY'), + :token => ENV.fetch('TRELLO_TOKEN') + } + response = RestClient.get(uri.to_s) + boards = JSON.parse(response.body) + boards.each do |board| + board_id = board['id'] + board_name = board['name'] sns.publish( topic_arn: TOPIC_ARN, subject: "Backup Trello board", - message: "Backup Trello board: #{board.name}", + message: "Backup Trello board: #{board_name}", message_attributes: { 'board_id' => { data_type: 'String', - string_value: board.id + string_value: board_id }, 'board_name' => { data_type: 'String', - string_value: board.name + string_value: board_name } } ) From ff86d0ce3d436ca52358d507f6f961a5b6869501 Mon Sep 17 00:00:00 2001 From: James Mead <3169+floehopper@users.noreply.github.com> Date: Sun, 23 Dec 2018 22:34:26 +0000 Subject: [PATCH 39/57] Use Net::HTTP vs addressable & rest-client gems to reduce dependencies And reduce the size of the zip file uploaded to AWS. --- lambdaFunctions/backupTrelloBoard/Gemfile | 2 -- .../backupTrelloBoard/Gemfile.lock | 20 ------------------- lambdaFunctions/backupTrelloBoard/index.rb | 13 ++++++------ lambdaFunctions/enumerateTrelloBoards/Gemfile | 2 -- .../enumerateTrelloBoards/Gemfile.lock | 20 ------------------- .../enumerateTrelloBoards/index.rb | 13 ++++++------ 6 files changed, 14 insertions(+), 56 deletions(-) diff --git a/lambdaFunctions/backupTrelloBoard/Gemfile b/lambdaFunctions/backupTrelloBoard/Gemfile index 0a2f33f..8716109 100644 --- a/lambdaFunctions/backupTrelloBoard/Gemfile +++ b/lambdaFunctions/backupTrelloBoard/Gemfile @@ -2,7 +2,5 @@ source 'https://rubygems.org' ruby '~> 2.5.0' -gem 'addressable' gem 'aws-sdk-s3' gem 'dotenv' -gem 'rest-client' diff --git a/lambdaFunctions/backupTrelloBoard/Gemfile.lock b/lambdaFunctions/backupTrelloBoard/Gemfile.lock index 2d335ca..f3df378 100644 --- a/lambdaFunctions/backupTrelloBoard/Gemfile.lock +++ b/lambdaFunctions/backupTrelloBoard/Gemfile.lock @@ -1,8 +1,6 @@ GEM remote: https://rubygems.org/ specs: - addressable (2.5.2) - public_suffix (>= 2.0.2, < 4.0) aws-eventstream (1.0.1) aws-partitions (1.125.0) aws-sdk-core (3.44.0) @@ -18,33 +16,15 @@ GEM aws-sdk-kms (~> 1) aws-sigv4 (~> 1.0) aws-sigv4 (1.0.3) - domain_name (0.5.20180417) - unf (>= 0.0.5, < 1.0.0) dotenv (2.5.0) - http-cookie (1.0.3) - domain_name (~> 0.5) jmespath (1.4.0) - mime-types (3.2.2) - mime-types-data (~> 3.2015) - mime-types-data (3.2018.0812) - netrc (0.11.0) - public_suffix (3.0.3) - rest-client (2.0.2) - http-cookie (>= 1.0.2, < 2.0) - mime-types (>= 1.16, < 4.0) - netrc (~> 0.8) - unf (0.1.4) - unf_ext - unf_ext (0.0.7.5) PLATFORMS ruby DEPENDENCIES - addressable aws-sdk-s3 dotenv - rest-client RUBY VERSION ruby 2.5.3p105 diff --git a/lambdaFunctions/backupTrelloBoard/index.rb b/lambdaFunctions/backupTrelloBoard/index.rb index 6ec8879..b8d5bd0 100644 --- a/lambdaFunctions/backupTrelloBoard/index.rb +++ b/lambdaFunctions/backupTrelloBoard/index.rb @@ -1,7 +1,5 @@ -require 'addressable/uri' require 'aws-sdk-s3' require 'dotenv' -require 'restclient' Dotenv.load @@ -16,8 +14,8 @@ def handler(event:, context:) board_name = attributes.dig('board_name', 'Value') endpoint = "https://api.trello.com/1/boards/#{board_id}" - uri = Addressable::URI.parse(endpoint) - uri.query_values = { + uri = URI(endpoint) + uri.query = URI.encode_www_form({ :actions => :all, :actions_limit => 1000, :cards => :all, @@ -29,10 +27,13 @@ def handler(event:, context:) :card_attachments => true, :key => ENV.fetch('TRELLO_KEY'), :token => ENV.fetch('TRELLO_TOKEN') - } + }) puts "#{board_id} - Fetching Trello data for #{board_name} board..." - response = RestClient.get(uri.to_s) + response = Net::HTTP.get_response(uri) + unless response.is_a?(Net::HTTPSuccess) + raise "Trello API error: #{response.message} #{response.code}" + end json = response.body puts "#{board_id} - OK" diff --git a/lambdaFunctions/enumerateTrelloBoards/Gemfile b/lambdaFunctions/enumerateTrelloBoards/Gemfile index 5a6de57..6918dec 100644 --- a/lambdaFunctions/enumerateTrelloBoards/Gemfile +++ b/lambdaFunctions/enumerateTrelloBoards/Gemfile @@ -2,7 +2,5 @@ source 'https://rubygems.org' ruby '~> 2.5.0' -gem 'addressable' gem 'aws-sdk-sns' gem 'dotenv' -gem 'rest-client' diff --git a/lambdaFunctions/enumerateTrelloBoards/Gemfile.lock b/lambdaFunctions/enumerateTrelloBoards/Gemfile.lock index 1126abf..01b272f 100644 --- a/lambdaFunctions/enumerateTrelloBoards/Gemfile.lock +++ b/lambdaFunctions/enumerateTrelloBoards/Gemfile.lock @@ -1,8 +1,6 @@ GEM remote: https://rubygems.org/ specs: - addressable (2.5.2) - public_suffix (>= 2.0.2, < 4.0) aws-eventstream (1.0.1) aws-partitions (1.125.0) aws-sdk-core (3.44.0) @@ -14,33 +12,15 @@ GEM aws-sdk-core (~> 3, >= 3.39.0) aws-sigv4 (~> 1.0) aws-sigv4 (1.0.3) - domain_name (0.5.20180417) - unf (>= 0.0.5, < 1.0.0) dotenv (2.5.0) - http-cookie (1.0.3) - domain_name (~> 0.5) jmespath (1.4.0) - mime-types (3.2.2) - mime-types-data (~> 3.2015) - mime-types-data (3.2018.0812) - netrc (0.11.0) - public_suffix (3.0.3) - rest-client (2.0.2) - http-cookie (>= 1.0.2, < 2.0) - mime-types (>= 1.16, < 4.0) - netrc (~> 0.8) - unf (0.1.4) - unf_ext - unf_ext (0.0.7.5) PLATFORMS ruby DEPENDENCIES - addressable aws-sdk-sns dotenv - rest-client RUBY VERSION ruby 2.5.3p105 diff --git a/lambdaFunctions/enumerateTrelloBoards/index.rb b/lambdaFunctions/enumerateTrelloBoards/index.rb index 0cfbd62..a102015 100644 --- a/lambdaFunctions/enumerateTrelloBoards/index.rb +++ b/lambdaFunctions/enumerateTrelloBoards/index.rb @@ -1,7 +1,5 @@ -require 'addressable/uri' require 'aws-sdk-sns' require 'dotenv' -require 'restclient' Dotenv.load @@ -11,12 +9,15 @@ def handler(event:, context:) sns = Aws::SNS::Client.new endpoint = "https://api.trello.com/1/members/me/boards" - uri = Addressable::URI.parse(endpoint) - uri.query_values = { + uri = URI(endpoint) + uri.query = URI.encode_www_form({ :key => ENV.fetch('TRELLO_KEY'), :token => ENV.fetch('TRELLO_TOKEN') - } - response = RestClient.get(uri.to_s) + }) + response = Net::HTTP.get_response(uri) + unless response.is_a?(Net::HTTPSuccess) + raise "Trello API error: #{response.message} #{response.code}" + end boards = JSON.parse(response.body) boards.each do |board| From 8b5a4e706c283346d482724d91eb5b59a9b855e2 Mon Sep 17 00:00:00 2001 From: James Mead <3169+floehopper@users.noreply.github.com> Date: Mon, 24 Dec 2018 11:42:05 +0000 Subject: [PATCH 40/57] Use single .env file & copy it into lambda function directories --- README.md | 6 ++++-- lambdaFunctions/backupTrelloBoard/build.sh | 1 + lambdaFunctions/enumerateTrelloBoards/build.sh | 1 + 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 7de3332..e3993a9 100644 --- a/README.md +++ b/README.md @@ -46,8 +46,8 @@ Ensure you're logged in as the GFR Admin user on https://trello.com. It's safe t $ export TRELLO_TOKEN=`pbpaste` # Store the Trello token in .env - $ echo "TRELLO_TOKEN=$TRELLO_TOKEN" >> lambdaFunctions/enumerateTrelloBoards/.env - $ echo "TRELLO_TOKEN=$TRELLO_TOKEN" >> lambdaFunctions/backupTrelloBoard/.env + $ echo "TRELLO_TOKEN=$TRELLO_TOKEN" >> .env + $ echo "TRELLO_TOKEN=$TRELLO_TOKEN" >> .env ### Environment variables @@ -57,6 +57,8 @@ In the top-level `.env` file, set the following environment variables: * `TRELLO_BOARD_BACKUPS_BUCKET_NAME` - the name of the S3 bucket you want to be created and used to save backups * `TRELLO_BOARD_BACKUPS_ALARM_EMAIL_ADDRESS` - the email address where alarm emails will be sent (an email will be sent on deployment to ask you to confirm the subscription) +Note: this `.env` file is copied into some/all of the `lambdaFunctions` folders at build time. + ## Build npm run build diff --git a/lambdaFunctions/backupTrelloBoard/build.sh b/lambdaFunctions/backupTrelloBoard/build.sh index 8edff89..9c37336 100755 --- a/lambdaFunctions/backupTrelloBoard/build.sh +++ b/lambdaFunctions/backupTrelloBoard/build.sh @@ -1,4 +1,5 @@ #!/usr/bin/env bash set -e +cp ../../.env . rm -rf vendor/bundle bundle install --path vendor/bundle diff --git a/lambdaFunctions/enumerateTrelloBoards/build.sh b/lambdaFunctions/enumerateTrelloBoards/build.sh index 8edff89..9c37336 100755 --- a/lambdaFunctions/enumerateTrelloBoards/build.sh +++ b/lambdaFunctions/enumerateTrelloBoards/build.sh @@ -1,4 +1,5 @@ #!/usr/bin/env bash set -e +cp ../../.env . rm -rf vendor/bundle bundle install --path vendor/bundle From dd6e01b0c2163a407b862f7f13413ff8a6a3a706 Mon Sep 17 00:00:00 2001 From: James Mead <3169+floehopper@users.noreply.github.com> Date: Mon, 24 Dec 2018 15:11:48 +0000 Subject: [PATCH 41/57] Add missing labels parameter when downloading board JSON I noticed this was missing when I was comparing this project with the `trello-full-backup` Python package [1]. [1]: https://pypi.org/project/trello-full-backup/ --- lambdaFunctions/backupTrelloBoard/index.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/lambdaFunctions/backupTrelloBoard/index.rb b/lambdaFunctions/backupTrelloBoard/index.rb index b8d5bd0..30c91a9 100644 --- a/lambdaFunctions/backupTrelloBoard/index.rb +++ b/lambdaFunctions/backupTrelloBoard/index.rb @@ -19,6 +19,7 @@ def handler(event:, context:) :actions => :all, :actions_limit => 1000, :cards => :all, + :labels => :all, :lists => :all, :members => :all, :member_fields => :all, From 41ccd14d037feb5869f25385942c5b29ca32d754 Mon Sep 17 00:00:00 2001 From: James Mead <3169+floehopper@users.noreply.github.com> Date: Mon, 24 Dec 2018 15:11:51 +0000 Subject: [PATCH 42/57] Re-order board query parameters Move key & token to beginning of list and order the remaining keys alphabetically. --- lambdaFunctions/backupTrelloBoard/index.rb | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/lambdaFunctions/backupTrelloBoard/index.rb b/lambdaFunctions/backupTrelloBoard/index.rb index 30c91a9..0ec4812 100644 --- a/lambdaFunctions/backupTrelloBoard/index.rb +++ b/lambdaFunctions/backupTrelloBoard/index.rb @@ -16,18 +16,18 @@ def handler(event:, context:) endpoint = "https://api.trello.com/1/boards/#{board_id}" uri = URI(endpoint) uri.query = URI.encode_www_form({ + :key => ENV.fetch('TRELLO_KEY'), + :token => ENV.fetch('TRELLO_TOKEN'), :actions => :all, :actions_limit => 1000, :cards => :all, + :card_attachments => true, + :checklists => :all, + :fields => :all, :labels => :all, :lists => :all, :members => :all, - :member_fields => :all, - :checklists => :all, - :fields => :all, - :card_attachments => true, - :key => ENV.fetch('TRELLO_KEY'), - :token => ENV.fetch('TRELLO_TOKEN') + :member_fields => :all }) puts "#{board_id} - Fetching Trello data for #{board_name} board..." From 9f20085a80c75afade38a657308493c301eb59d9 Mon Sep 17 00:00:00 2001 From: James Mead <3169+floehopper@users.noreply.github.com> Date: Fri, 28 Dec 2018 20:13:43 +0000 Subject: [PATCH 43/57] Restrict fields returned from Trello API boards endpoint This should hopefully speed up the API response time. --- lambdaFunctions/enumerateTrelloBoards/index.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lambdaFunctions/enumerateTrelloBoards/index.rb b/lambdaFunctions/enumerateTrelloBoards/index.rb index a102015..735fe80 100644 --- a/lambdaFunctions/enumerateTrelloBoards/index.rb +++ b/lambdaFunctions/enumerateTrelloBoards/index.rb @@ -12,7 +12,8 @@ def handler(event:, context:) uri = URI(endpoint) uri.query = URI.encode_www_form({ :key => ENV.fetch('TRELLO_KEY'), - :token => ENV.fetch('TRELLO_TOKEN') + :token => ENV.fetch('TRELLO_TOKEN'), + :fields => 'id,name' }) response = Net::HTTP.get_response(uri) unless response.is_a?(Net::HTTPSuccess) From e218ae46bd02cb19e157dfbc4ce6a6b026f9ea65 Mon Sep 17 00:00:00 2001 From: James Mead <3169+floehopper@users.noreply.github.com> Date: Sat, 29 Dec 2018 11:27:26 +0000 Subject: [PATCH 44/57] Avoid duplication in S3 bucket name env var --- index.ts | 3 --- lambdaFunctions/backupTrelloBoard/index.rb | 2 +- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/index.ts b/index.ts index af8f1e5..9a87f5f 100644 --- a/index.ts +++ b/index.ts @@ -63,9 +63,6 @@ class TrelloBackupStack extends cdk.Stack { runtime: rubyLambdaRuntime, handler: 'index.handler', code: lambda.Code.asset('./lambdaFunctions/backupTrelloBoard'), - environment: { - BACKUP_TRELLO_BOARD_S3_BUCKET_NAME: trelloBoardBackupsBucket.bucketName - }, timeout: lambdaFunctionTimeout }); trelloBoardBackupsBucket.grantPut(lambdaFunction.role); diff --git a/lambdaFunctions/backupTrelloBoard/index.rb b/lambdaFunctions/backupTrelloBoard/index.rb index 0ec4812..d38d32f 100644 --- a/lambdaFunctions/backupTrelloBoard/index.rb +++ b/lambdaFunctions/backupTrelloBoard/index.rb @@ -3,7 +3,7 @@ Dotenv.load -S3_BUCKET_NAME = ENV.fetch('BACKUP_TRELLO_BOARD_S3_BUCKET_NAME') +S3_BUCKET_NAME = ENV.fetch('TRELLO_BOARD_BACKUPS_BUCKET_NAME') def handler(event:, context:) records = event.fetch('Records', []) From a7c196f358e0ced8f37dd4daad6c92ba49c4e08f Mon Sep 17 00:00:00 2001 From: James Mead <3169+floehopper@users.noreply.github.com> Date: Sat, 29 Dec 2018 11:29:31 +0000 Subject: [PATCH 45/57] Rename S3 bucket name env var to include "S3" To make its purpose clearer. --- README.md | 2 +- index.ts | 2 +- lambdaFunctions/backupTrelloBoard/index.rb | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index e3993a9..d862cae 100644 --- a/README.md +++ b/README.md @@ -54,7 +54,7 @@ Ensure you're logged in as the GFR Admin user on https://trello.com. It's safe t In the top-level `.env` file, set the following environment variables: * `TRELLO_BOARD_BACKUPS_SCHEDULE_EXPRESSION` - specifies how often the backup is performed, e.g. "cron(0 2 * * ? *)" runs daily at 2am (see [Schedule Expressions for Rules][3] for details) -* `TRELLO_BOARD_BACKUPS_BUCKET_NAME` - the name of the S3 bucket you want to be created and used to save backups +* `TRELLO_BOARD_BACKUPS_S3_BUCKET_NAME` - the name of the S3 bucket you want to be created and used to save backups * `TRELLO_BOARD_BACKUPS_ALARM_EMAIL_ADDRESS` - the email address where alarm emails will be sent (an email will be sent on deployment to ask you to confirm the subscription) Note: this `.env` file is copied into some/all of the `lambdaFunctions` folders at build time. diff --git a/index.ts b/index.ts index 9a87f5f..a8625f6 100644 --- a/index.ts +++ b/index.ts @@ -20,7 +20,7 @@ class TrelloBackupStack extends cdk.Stack { const enumerateTrelloBoardsFunction = this.createEnumerateTrelloBoardsFunction(backupTrelloBoardTopic, alarmTopic); - const bucketName = process.env.TRELLO_BOARD_BACKUPS_BUCKET_NAME; + const bucketName = process.env.TRELLO_BOARD_BACKUPS_S3_BUCKET_NAME; const trelloBoardBackupsBucket = this.createTrelloBoardBackupsBucket(bucketName); diff --git a/lambdaFunctions/backupTrelloBoard/index.rb b/lambdaFunctions/backupTrelloBoard/index.rb index d38d32f..85056c1 100644 --- a/lambdaFunctions/backupTrelloBoard/index.rb +++ b/lambdaFunctions/backupTrelloBoard/index.rb @@ -3,7 +3,7 @@ Dotenv.load -S3_BUCKET_NAME = ENV.fetch('TRELLO_BOARD_BACKUPS_BUCKET_NAME') +S3_BUCKET_NAME = ENV.fetch('TRELLO_BOARD_BACKUPS_S3_BUCKET_NAME') def handler(event:, context:) records = event.fetch('Records', []) From 1c1a0ffe4fcf027ec180ad015bb7fb9e0da0ec12 Mon Sep 17 00:00:00 2001 From: James Mead <3169+floehopper@users.noreply.github.com> Date: Sat, 29 Dec 2018 11:38:08 +0000 Subject: [PATCH 46/57] Rename alarm topic to monitoring topic I'm planning to use this topic for monitoring as well as alarms. --- README.md | 2 +- index.ts | 22 +++++++++++----------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index d862cae..3c5aff9 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,7 @@ In the top-level `.env` file, set the following environment variables: * `TRELLO_BOARD_BACKUPS_SCHEDULE_EXPRESSION` - specifies how often the backup is performed, e.g. "cron(0 2 * * ? *)" runs daily at 2am (see [Schedule Expressions for Rules][3] for details) * `TRELLO_BOARD_BACKUPS_S3_BUCKET_NAME` - the name of the S3 bucket you want to be created and used to save backups -* `TRELLO_BOARD_BACKUPS_ALARM_EMAIL_ADDRESS` - the email address where alarm emails will be sent (an email will be sent on deployment to ask you to confirm the subscription) +* `TRELLO_BOARD_BACKUPS_MONITORING_EMAIL_ADDRESS` - the email address where monitoring emails will be sent (an email will be sent on deployment to ask you to confirm the subscription) Note: this `.env` file is copied into some/all of the `lambdaFunctions` folders at build time. diff --git a/index.ts b/index.ts index a8625f6..0008502 100644 --- a/index.ts +++ b/index.ts @@ -14,29 +14,29 @@ class TrelloBackupStack extends cdk.Stack { constructor(parent: cdk.App, id: string, props?: cdk.StackProps) { super(parent, id, props); - const alarmTopic = new Topic(this, 'alarmTopic'); + const monitoringTopic = new Topic(this, 'monitoringTopic'); const backupTrelloBoardTopic = new Topic(this, 'backupTrelloBoardTopic'); const enumerateTrelloBoardsFunction - = this.createEnumerateTrelloBoardsFunction(backupTrelloBoardTopic, alarmTopic); + = this.createEnumerateTrelloBoardsFunction(backupTrelloBoardTopic, monitoringTopic); const bucketName = process.env.TRELLO_BOARD_BACKUPS_S3_BUCKET_NAME; const trelloBoardBackupsBucket = this.createTrelloBoardBackupsBucket(bucketName); const backupTrelloBoardFunction - = this.createBackupTrelloBoardFunction(trelloBoardBackupsBucket, alarmTopic); + = this.createBackupTrelloBoardFunction(trelloBoardBackupsBucket, monitoringTopic); backupTrelloBoardTopic.subscribeLambda(backupTrelloBoardFunction); - const alarmEmailAddress = process.env.TRELLO_BOARD_BACKUPS_ALARM_EMAIL_ADDRESS; - alarmTopic.subscribeEmail('alarmTopicEmail', alarmEmailAddress); + const monitoringEmailAddress = process.env.TRELLO_BOARD_BACKUPS_MONITORING_EMAIL_ADDRESS; + monitoringTopic.subscribeEmail('monitoringTopicEmail', monitoringEmailAddress); const scheduleExpression = process.env.TRELLO_BOARD_BACKUPS_SCHEDULE_EXPRESSION; this.schedule(enumerateTrelloBoardsFunction, scheduleExpression); } - createEnumerateTrelloBoardsFunction(backupTrelloBoardTopic : Topic, alarmTopic : Topic) : lambda.Function { + createEnumerateTrelloBoardsFunction(backupTrelloBoardTopic : Topic, monitoringTopic : Topic) : lambda.Function { const lambdaFunction = new lambda.Function(this, 'enumerateTrelloBoards', { runtime: rubyLambdaRuntime, handler: 'index.handler', @@ -47,7 +47,7 @@ class TrelloBackupStack extends cdk.Stack { timeout: lambdaFunctionTimeout }); backupTrelloBoardTopic.grantPublish(lambdaFunction.role); - this.reportErrors(lambdaFunction, alarmTopic); + this.reportErrors(lambdaFunction, monitoringTopic); return lambdaFunction; } @@ -58,7 +58,7 @@ class TrelloBackupStack extends cdk.Stack { }); } - createBackupTrelloBoardFunction(trelloBoardBackupsBucket : s3.Bucket, alarmTopic : Topic) : lambda.Function { + createBackupTrelloBoardFunction(trelloBoardBackupsBucket : s3.Bucket, monitoringTopic : Topic) : lambda.Function { const lambdaFunction = new lambda.Function(this, 'backupTrelloBoard', { runtime: rubyLambdaRuntime, handler: 'index.handler', @@ -66,7 +66,7 @@ class TrelloBackupStack extends cdk.Stack { timeout: lambdaFunctionTimeout }); trelloBoardBackupsBucket.grantPut(lambdaFunction.role); - this.reportErrors(lambdaFunction, alarmTopic); + this.reportErrors(lambdaFunction, monitoringTopic); return lambdaFunction; } @@ -77,13 +77,13 @@ class TrelloBackupStack extends cdk.Stack { rule.addTarget(lambdaFunction); } - reportErrors(lambdaFunction : lambda.Function, alarmTopic : Topic) : void { + reportErrors(lambdaFunction : lambda.Function, monitoringTopic : Topic) : void { const metricErrors = lambdaFunction.metricErrors(); const alarm = metricErrors.newAlarm(this, `${lambdaFunction.id}Alarm`, { threshold: 1, evaluationPeriods: 1 }); - alarm.onAlarm(alarmTopic); + alarm.onAlarm(monitoringTopic); } } From d552bf6a8c26733c39b4fc83b58d472133834a2b Mon Sep 17 00:00:00 2001 From: James Mead <3169+floehopper@users.noreply.github.com> Date: Sat, 29 Dec 2018 11:40:32 +0000 Subject: [PATCH 47/57] Make it clear when SNS subscription confirmation is sent --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 3c5aff9..24afce4 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,7 @@ In the top-level `.env` file, set the following environment variables: * `TRELLO_BOARD_BACKUPS_SCHEDULE_EXPRESSION` - specifies how often the backup is performed, e.g. "cron(0 2 * * ? *)" runs daily at 2am (see [Schedule Expressions for Rules][3] for details) * `TRELLO_BOARD_BACKUPS_S3_BUCKET_NAME` - the name of the S3 bucket you want to be created and used to save backups -* `TRELLO_BOARD_BACKUPS_MONITORING_EMAIL_ADDRESS` - the email address where monitoring emails will be sent (an email will be sent on deployment to ask you to confirm the subscription) +* `TRELLO_BOARD_BACKUPS_MONITORING_EMAIL_ADDRESS` - the email address where monitoring emails will be sent (an email will be sent on first deployment to ask you to confirm the subscription) Note: this `.env` file is copied into some/all of the `lambdaFunctions` folders at build time. From b63e32496e6ea3d8fa88dee22eab868ba4231211 Mon Sep 17 00:00:00 2001 From: James Mead <3169+floehopper@users.noreply.github.com> Date: Sat, 29 Dec 2018 11:41:14 +0000 Subject: [PATCH 48/57] Fix Markdown in README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 24afce4..0cbe302 100644 --- a/README.md +++ b/README.md @@ -53,7 +53,7 @@ Ensure you're logged in as the GFR Admin user on https://trello.com. It's safe t In the top-level `.env` file, set the following environment variables: -* `TRELLO_BOARD_BACKUPS_SCHEDULE_EXPRESSION` - specifies how often the backup is performed, e.g. "cron(0 2 * * ? *)" runs daily at 2am (see [Schedule Expressions for Rules][3] for details) +* `TRELLO_BOARD_BACKUPS_SCHEDULE_EXPRESSION` - specifies how often the backup is performed, e.g. `cron(0 2 * * ? *)` runs daily at 2am (see [Schedule Expressions for Rules][3] for details) * `TRELLO_BOARD_BACKUPS_S3_BUCKET_NAME` - the name of the S3 bucket you want to be created and used to save backups * `TRELLO_BOARD_BACKUPS_MONITORING_EMAIL_ADDRESS` - the email address where monitoring emails will be sent (an email will be sent on first deployment to ask you to confirm the subscription) From df0d545f57ad3024f84ded8902048916a20f5e41 Mon Sep 17 00:00:00 2001 From: James Mead <3169+floehopper@users.noreply.github.com> Date: Sat, 29 Dec 2018 12:07:04 +0000 Subject: [PATCH 49/57] Rename backupTrelloBoardTopic -> backupBoardTopic No need to include "Trello" in constant name. --- index.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/index.ts b/index.ts index 0008502..bef2cc4 100644 --- a/index.ts +++ b/index.ts @@ -15,10 +15,10 @@ class TrelloBackupStack extends cdk.Stack { super(parent, id, props); const monitoringTopic = new Topic(this, 'monitoringTopic'); - const backupTrelloBoardTopic = new Topic(this, 'backupTrelloBoardTopic'); + const backupBoardTopic = new Topic(this, 'backupBoardTopic'); const enumerateTrelloBoardsFunction - = this.createEnumerateTrelloBoardsFunction(backupTrelloBoardTopic, monitoringTopic); + = this.createEnumerateTrelloBoardsFunction(backupBoardTopic, monitoringTopic); const bucketName = process.env.TRELLO_BOARD_BACKUPS_S3_BUCKET_NAME; const trelloBoardBackupsBucket @@ -27,7 +27,7 @@ class TrelloBackupStack extends cdk.Stack { const backupTrelloBoardFunction = this.createBackupTrelloBoardFunction(trelloBoardBackupsBucket, monitoringTopic); - backupTrelloBoardTopic.subscribeLambda(backupTrelloBoardFunction); + backupBoardTopic.subscribeLambda(backupTrelloBoardFunction); const monitoringEmailAddress = process.env.TRELLO_BOARD_BACKUPS_MONITORING_EMAIL_ADDRESS; monitoringTopic.subscribeEmail('monitoringTopicEmail', monitoringEmailAddress); @@ -36,17 +36,17 @@ class TrelloBackupStack extends cdk.Stack { this.schedule(enumerateTrelloBoardsFunction, scheduleExpression); } - createEnumerateTrelloBoardsFunction(backupTrelloBoardTopic : Topic, monitoringTopic : Topic) : lambda.Function { + createEnumerateTrelloBoardsFunction(backupBoardTopic : Topic, monitoringTopic : Topic) : lambda.Function { const lambdaFunction = new lambda.Function(this, 'enumerateTrelloBoards', { runtime: rubyLambdaRuntime, handler: 'index.handler', code: lambda.Code.asset('./lambdaFunctions/enumerateTrelloBoards'), environment: { - BACKUP_TRELLO_BOARD_TOPIC_ARN: backupTrelloBoardTopic.topicArn + BACKUP_TRELLO_BOARD_TOPIC_ARN: backupBoardTopic.topicArn }, timeout: lambdaFunctionTimeout }); - backupTrelloBoardTopic.grantPublish(lambdaFunction.role); + backupBoardTopic.grantPublish(lambdaFunction.role); this.reportErrors(lambdaFunction, monitoringTopic); return lambdaFunction; } From 5145032a4db94ab67662ba0c460137b5c911a094 Mon Sep 17 00:00:00 2001 From: James Mead <3169+floehopper@users.noreply.github.com> Date: Sat, 29 Dec 2018 12:09:27 +0000 Subject: [PATCH 50/57] Rename enumerateTrelloBoardsFunction -> enumerateBoardsFunction No need to repeat "Trello" in constant/function name. --- index.ts | 12 ++++++------ .../.gitignore | 0 .../Gemfile | 0 .../Gemfile.lock | 0 .../build.sh | 0 .../index.rb | 0 6 files changed, 6 insertions(+), 6 deletions(-) rename lambdaFunctions/{enumerateTrelloBoards => enumerateBoards}/.gitignore (100%) rename lambdaFunctions/{enumerateTrelloBoards => enumerateBoards}/Gemfile (100%) rename lambdaFunctions/{enumerateTrelloBoards => enumerateBoards}/Gemfile.lock (100%) rename lambdaFunctions/{enumerateTrelloBoards => enumerateBoards}/build.sh (100%) rename lambdaFunctions/{enumerateTrelloBoards => enumerateBoards}/index.rb (100%) diff --git a/index.ts b/index.ts index bef2cc4..cf96b9c 100644 --- a/index.ts +++ b/index.ts @@ -17,8 +17,8 @@ class TrelloBackupStack extends cdk.Stack { const monitoringTopic = new Topic(this, 'monitoringTopic'); const backupBoardTopic = new Topic(this, 'backupBoardTopic'); - const enumerateTrelloBoardsFunction - = this.createEnumerateTrelloBoardsFunction(backupBoardTopic, monitoringTopic); + const enumerateBoardsFunction + = this.createEnumerateBoardsFunction(backupBoardTopic, monitoringTopic); const bucketName = process.env.TRELLO_BOARD_BACKUPS_S3_BUCKET_NAME; const trelloBoardBackupsBucket @@ -33,14 +33,14 @@ class TrelloBackupStack extends cdk.Stack { monitoringTopic.subscribeEmail('monitoringTopicEmail', monitoringEmailAddress); const scheduleExpression = process.env.TRELLO_BOARD_BACKUPS_SCHEDULE_EXPRESSION; - this.schedule(enumerateTrelloBoardsFunction, scheduleExpression); + this.schedule(enumerateBoardsFunction, scheduleExpression); } - createEnumerateTrelloBoardsFunction(backupBoardTopic : Topic, monitoringTopic : Topic) : lambda.Function { - const lambdaFunction = new lambda.Function(this, 'enumerateTrelloBoards', { + createEnumerateBoardsFunction(backupBoardTopic : Topic, monitoringTopic : Topic) : lambda.Function { + const lambdaFunction = new lambda.Function(this, 'enumerateBoards', { runtime: rubyLambdaRuntime, handler: 'index.handler', - code: lambda.Code.asset('./lambdaFunctions/enumerateTrelloBoards'), + code: lambda.Code.asset('./lambdaFunctions/enumerateBoards'), environment: { BACKUP_TRELLO_BOARD_TOPIC_ARN: backupBoardTopic.topicArn }, diff --git a/lambdaFunctions/enumerateTrelloBoards/.gitignore b/lambdaFunctions/enumerateBoards/.gitignore similarity index 100% rename from lambdaFunctions/enumerateTrelloBoards/.gitignore rename to lambdaFunctions/enumerateBoards/.gitignore diff --git a/lambdaFunctions/enumerateTrelloBoards/Gemfile b/lambdaFunctions/enumerateBoards/Gemfile similarity index 100% rename from lambdaFunctions/enumerateTrelloBoards/Gemfile rename to lambdaFunctions/enumerateBoards/Gemfile diff --git a/lambdaFunctions/enumerateTrelloBoards/Gemfile.lock b/lambdaFunctions/enumerateBoards/Gemfile.lock similarity index 100% rename from lambdaFunctions/enumerateTrelloBoards/Gemfile.lock rename to lambdaFunctions/enumerateBoards/Gemfile.lock diff --git a/lambdaFunctions/enumerateTrelloBoards/build.sh b/lambdaFunctions/enumerateBoards/build.sh similarity index 100% rename from lambdaFunctions/enumerateTrelloBoards/build.sh rename to lambdaFunctions/enumerateBoards/build.sh diff --git a/lambdaFunctions/enumerateTrelloBoards/index.rb b/lambdaFunctions/enumerateBoards/index.rb similarity index 100% rename from lambdaFunctions/enumerateTrelloBoards/index.rb rename to lambdaFunctions/enumerateBoards/index.rb From 27b6139deecbc2fa3ab011a329d76b16fc29f2db Mon Sep 17 00:00:00 2001 From: James Mead <3169+floehopper@users.noreply.github.com> Date: Sat, 29 Dec 2018 12:12:13 +0000 Subject: [PATCH 51/57] Rename backupTrelloBoardFunction -> backupBoardFunction No need to repeat "Trello" in constant/function name. --- index.ts | 12 ++++++------ .../{backupTrelloBoard => backupBoard}/.gitignore | 0 .../{backupTrelloBoard => backupBoard}/Gemfile | 0 .../{backupTrelloBoard => backupBoard}/Gemfile.lock | 0 .../{backupTrelloBoard => backupBoard}/build.sh | 0 .../{backupTrelloBoard => backupBoard}/index.rb | 0 6 files changed, 6 insertions(+), 6 deletions(-) rename lambdaFunctions/{backupTrelloBoard => backupBoard}/.gitignore (100%) rename lambdaFunctions/{backupTrelloBoard => backupBoard}/Gemfile (100%) rename lambdaFunctions/{backupTrelloBoard => backupBoard}/Gemfile.lock (100%) rename lambdaFunctions/{backupTrelloBoard => backupBoard}/build.sh (100%) rename lambdaFunctions/{backupTrelloBoard => backupBoard}/index.rb (100%) diff --git a/index.ts b/index.ts index cf96b9c..11bafcc 100644 --- a/index.ts +++ b/index.ts @@ -24,10 +24,10 @@ class TrelloBackupStack extends cdk.Stack { const trelloBoardBackupsBucket = this.createTrelloBoardBackupsBucket(bucketName); - const backupTrelloBoardFunction - = this.createBackupTrelloBoardFunction(trelloBoardBackupsBucket, monitoringTopic); + const backupBoardFunction + = this.createBackupBoardFunction(trelloBoardBackupsBucket, monitoringTopic); - backupBoardTopic.subscribeLambda(backupTrelloBoardFunction); + backupBoardTopic.subscribeLambda(backupBoardFunction); const monitoringEmailAddress = process.env.TRELLO_BOARD_BACKUPS_MONITORING_EMAIL_ADDRESS; monitoringTopic.subscribeEmail('monitoringTopicEmail', monitoringEmailAddress); @@ -58,11 +58,11 @@ class TrelloBackupStack extends cdk.Stack { }); } - createBackupTrelloBoardFunction(trelloBoardBackupsBucket : s3.Bucket, monitoringTopic : Topic) : lambda.Function { - const lambdaFunction = new lambda.Function(this, 'backupTrelloBoard', { + createBackupBoardFunction(trelloBoardBackupsBucket : s3.Bucket, monitoringTopic : Topic) : lambda.Function { + const lambdaFunction = new lambda.Function(this, 'backupBoard', { runtime: rubyLambdaRuntime, handler: 'index.handler', - code: lambda.Code.asset('./lambdaFunctions/backupTrelloBoard'), + code: lambda.Code.asset('./lambdaFunctions/backupBoard'), timeout: lambdaFunctionTimeout }); trelloBoardBackupsBucket.grantPut(lambdaFunction.role); diff --git a/lambdaFunctions/backupTrelloBoard/.gitignore b/lambdaFunctions/backupBoard/.gitignore similarity index 100% rename from lambdaFunctions/backupTrelloBoard/.gitignore rename to lambdaFunctions/backupBoard/.gitignore diff --git a/lambdaFunctions/backupTrelloBoard/Gemfile b/lambdaFunctions/backupBoard/Gemfile similarity index 100% rename from lambdaFunctions/backupTrelloBoard/Gemfile rename to lambdaFunctions/backupBoard/Gemfile diff --git a/lambdaFunctions/backupTrelloBoard/Gemfile.lock b/lambdaFunctions/backupBoard/Gemfile.lock similarity index 100% rename from lambdaFunctions/backupTrelloBoard/Gemfile.lock rename to lambdaFunctions/backupBoard/Gemfile.lock diff --git a/lambdaFunctions/backupTrelloBoard/build.sh b/lambdaFunctions/backupBoard/build.sh similarity index 100% rename from lambdaFunctions/backupTrelloBoard/build.sh rename to lambdaFunctions/backupBoard/build.sh diff --git a/lambdaFunctions/backupTrelloBoard/index.rb b/lambdaFunctions/backupBoard/index.rb similarity index 100% rename from lambdaFunctions/backupTrelloBoard/index.rb rename to lambdaFunctions/backupBoard/index.rb From 59019173022bd0c6038d4cf0c1530eda9c298cf3 Mon Sep 17 00:00:00 2001 From: James Mead <3169+floehopper@users.noreply.github.com> Date: Sat, 29 Dec 2018 12:14:52 +0000 Subject: [PATCH 52/57] Rename trelloBoardBackupsBucket -> boardBackupsBucket No need to repeat "Trello" in constant/function name. --- index.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/index.ts b/index.ts index 11bafcc..ab9629b 100644 --- a/index.ts +++ b/index.ts @@ -21,11 +21,11 @@ class TrelloBackupStack extends cdk.Stack { = this.createEnumerateBoardsFunction(backupBoardTopic, monitoringTopic); const bucketName = process.env.TRELLO_BOARD_BACKUPS_S3_BUCKET_NAME; - const trelloBoardBackupsBucket - = this.createTrelloBoardBackupsBucket(bucketName); + const boardBackupsBucket + = this.createBoardBackupsBucket(bucketName); const backupBoardFunction - = this.createBackupBoardFunction(trelloBoardBackupsBucket, monitoringTopic); + = this.createBackupBoardFunction(boardBackupsBucket, monitoringTopic); backupBoardTopic.subscribeLambda(backupBoardFunction); @@ -51,21 +51,21 @@ class TrelloBackupStack extends cdk.Stack { return lambdaFunction; } - createTrelloBoardBackupsBucket(bucketName : string) : s3.Bucket { - return new s3.Bucket(this, 'trelloBoardBackupsBucket', { + createBoardBackupsBucket(bucketName : string) : s3.Bucket { + return new s3.Bucket(this, 'boardBackupsBucket', { bucketName: bucketName, versioned: true }); } - createBackupBoardFunction(trelloBoardBackupsBucket : s3.Bucket, monitoringTopic : Topic) : lambda.Function { + createBackupBoardFunction(boardBackupsBucket : s3.Bucket, monitoringTopic : Topic) : lambda.Function { const lambdaFunction = new lambda.Function(this, 'backupBoard', { runtime: rubyLambdaRuntime, handler: 'index.handler', code: lambda.Code.asset('./lambdaFunctions/backupBoard'), timeout: lambdaFunctionTimeout }); - trelloBoardBackupsBucket.grantPut(lambdaFunction.role); + boardBackupsBucket.grantPut(lambdaFunction.role); this.reportErrors(lambdaFunction, monitoringTopic); return lambdaFunction; } From ab8c11ab55c3bf391b83eb6f80a96451bae7cd4f Mon Sep 17 00:00:00 2001 From: James Mead <3169+floehopper@users.noreply.github.com> Date: Sat, 29 Dec 2018 12:19:57 +0000 Subject: [PATCH 53/57] Change env var prefix to match project name TRELLO_BOARD_BACKUPS -> TRELLO_BACKUP. --- README.md | 6 +++--- index.ts | 6 +++--- lambdaFunctions/backupBoard/index.rb | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 0cbe302..4db70d6 100644 --- a/README.md +++ b/README.md @@ -53,9 +53,9 @@ Ensure you're logged in as the GFR Admin user on https://trello.com. It's safe t In the top-level `.env` file, set the following environment variables: -* `TRELLO_BOARD_BACKUPS_SCHEDULE_EXPRESSION` - specifies how often the backup is performed, e.g. `cron(0 2 * * ? *)` runs daily at 2am (see [Schedule Expressions for Rules][3] for details) -* `TRELLO_BOARD_BACKUPS_S3_BUCKET_NAME` - the name of the S3 bucket you want to be created and used to save backups -* `TRELLO_BOARD_BACKUPS_MONITORING_EMAIL_ADDRESS` - the email address where monitoring emails will be sent (an email will be sent on first deployment to ask you to confirm the subscription) +* `TRELLO_BACKUP_SCHEDULE_EXPRESSION` - specifies how often the backup is performed, e.g. `cron(0 2 * * ? *)` runs daily at 2am (see [Schedule Expressions for Rules][3] for details) +* `TRELLO_BACKUP_S3_BUCKET_NAME` - the name of the S3 bucket you want to be created and used to save backups +* `TRELLO_BACKUP_MONITORING_EMAIL_ADDRESS` - the email address where monitoring emails will be sent (an email will be sent on first deployment to ask you to confirm the subscription) Note: this `.env` file is copied into some/all of the `lambdaFunctions` folders at build time. diff --git a/index.ts b/index.ts index ab9629b..8cee8bb 100644 --- a/index.ts +++ b/index.ts @@ -20,7 +20,7 @@ class TrelloBackupStack extends cdk.Stack { const enumerateBoardsFunction = this.createEnumerateBoardsFunction(backupBoardTopic, monitoringTopic); - const bucketName = process.env.TRELLO_BOARD_BACKUPS_S3_BUCKET_NAME; + const bucketName = process.env.TRELLO_BACKUP_S3_BUCKET_NAME; const boardBackupsBucket = this.createBoardBackupsBucket(bucketName); @@ -29,10 +29,10 @@ class TrelloBackupStack extends cdk.Stack { backupBoardTopic.subscribeLambda(backupBoardFunction); - const monitoringEmailAddress = process.env.TRELLO_BOARD_BACKUPS_MONITORING_EMAIL_ADDRESS; + const monitoringEmailAddress = process.env.TRELLO_BACKUP_MONITORING_EMAIL_ADDRESS; monitoringTopic.subscribeEmail('monitoringTopicEmail', monitoringEmailAddress); - const scheduleExpression = process.env.TRELLO_BOARD_BACKUPS_SCHEDULE_EXPRESSION; + const scheduleExpression = process.env.TRELLO_BACKUP_SCHEDULE_EXPRESSION; this.schedule(enumerateBoardsFunction, scheduleExpression); } diff --git a/lambdaFunctions/backupBoard/index.rb b/lambdaFunctions/backupBoard/index.rb index 85056c1..62a8d75 100644 --- a/lambdaFunctions/backupBoard/index.rb +++ b/lambdaFunctions/backupBoard/index.rb @@ -3,7 +3,7 @@ Dotenv.load -S3_BUCKET_NAME = ENV.fetch('TRELLO_BOARD_BACKUPS_S3_BUCKET_NAME') +S3_BUCKET_NAME = ENV.fetch('TRELLO_BACKUP_S3_BUCKET_NAME') def handler(event:, context:) records = event.fetch('Records', []) From 65cea17658df2d6506a693539ba4241d66255c87 Mon Sep 17 00:00:00 2001 From: James Mead <3169+floehopper@users.noreply.github.com> Date: Sat, 29 Dec 2018 12:21:59 +0000 Subject: [PATCH 54/57] Use project prefix even for dynamically generated env var BACKUP_TRELLO_BOARD_TOPIC_ARN -> TRELLO_BACKUP_BACKUP_BOARD_TOPIC_ARN. This makes the code more consistent. --- index.ts | 2 +- lambdaFunctions/enumerateBoards/index.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/index.ts b/index.ts index 8cee8bb..77f1898 100644 --- a/index.ts +++ b/index.ts @@ -42,7 +42,7 @@ class TrelloBackupStack extends cdk.Stack { handler: 'index.handler', code: lambda.Code.asset('./lambdaFunctions/enumerateBoards'), environment: { - BACKUP_TRELLO_BOARD_TOPIC_ARN: backupBoardTopic.topicArn + TRELLO_BACKUP_BACKUP_BOARD_TOPIC_ARN: backupBoardTopic.topicArn }, timeout: lambdaFunctionTimeout }); diff --git a/lambdaFunctions/enumerateBoards/index.rb b/lambdaFunctions/enumerateBoards/index.rb index 735fe80..8010711 100644 --- a/lambdaFunctions/enumerateBoards/index.rb +++ b/lambdaFunctions/enumerateBoards/index.rb @@ -3,7 +3,7 @@ Dotenv.load -TOPIC_ARN = ENV.fetch('BACKUP_TRELLO_BOARD_TOPIC_ARN') +TOPIC_ARN = ENV.fetch('TRELLO_BACKUP_BACKUP_BOARD_TOPIC_ARN') def handler(event:, context:) sns = Aws::SNS::Client.new From 7564a40764155092aa9128fe36e6dd46c5d19849 Mon Sep 17 00:00:00 2001 From: James Mead <3169+floehopper@users.noreply.github.com> Date: Sat, 29 Dec 2018 12:25:56 +0000 Subject: [PATCH 55/57] Rename schedule expression env var I want to introduce another schedule expression and this change which makes the name more specific will make that easier. --- README.md | 2 +- index.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 4db70d6..2003453 100644 --- a/README.md +++ b/README.md @@ -53,7 +53,7 @@ Ensure you're logged in as the GFR Admin user on https://trello.com. It's safe t In the top-level `.env` file, set the following environment variables: -* `TRELLO_BACKUP_SCHEDULE_EXPRESSION` - specifies how often the backup is performed, e.g. `cron(0 2 * * ? *)` runs daily at 2am (see [Schedule Expressions for Rules][3] for details) +* `TRELLO_BACKUP_SCHEDULE_FOR_BACKUP` - specifies how often the backup is performed, e.g. `cron(0 2 * * ? *)` runs daily at 2am (see [Schedule Expressions for Rules][3] for details) * `TRELLO_BACKUP_S3_BUCKET_NAME` - the name of the S3 bucket you want to be created and used to save backups * `TRELLO_BACKUP_MONITORING_EMAIL_ADDRESS` - the email address where monitoring emails will be sent (an email will be sent on first deployment to ask you to confirm the subscription) diff --git a/index.ts b/index.ts index 77f1898..e95fa95 100644 --- a/index.ts +++ b/index.ts @@ -32,8 +32,8 @@ class TrelloBackupStack extends cdk.Stack { const monitoringEmailAddress = process.env.TRELLO_BACKUP_MONITORING_EMAIL_ADDRESS; monitoringTopic.subscribeEmail('monitoringTopicEmail', monitoringEmailAddress); - const scheduleExpression = process.env.TRELLO_BACKUP_SCHEDULE_EXPRESSION; - this.schedule(enumerateBoardsFunction, scheduleExpression); + const scheduleForBackup = process.env.TRELLO_BACKUP_SCHEDULE_FOR_BACKUP; + this.schedule(enumerateBoardsFunction, scheduleForBackup); } createEnumerateBoardsFunction(backupBoardTopic : Topic, monitoringTopic : Topic) : lambda.Function { From 71327ee7208533079c1f3da1b6abd561720d17e5 Mon Sep 17 00:00:00 2001 From: James Mead <3169+floehopper@users.noreply.github.com> Date: Sat, 29 Dec 2018 12:32:16 +0000 Subject: [PATCH 56/57] Inline schedule function and use more specific naming Again this will make it easier to add another schedule expression rule. --- index.ts | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/index.ts b/index.ts index e95fa95..1455b05 100644 --- a/index.ts +++ b/index.ts @@ -33,7 +33,10 @@ class TrelloBackupStack extends cdk.Stack { monitoringTopic.subscribeEmail('monitoringTopicEmail', monitoringEmailAddress); const scheduleForBackup = process.env.TRELLO_BACKUP_SCHEDULE_FOR_BACKUP; - this.schedule(enumerateBoardsFunction, scheduleForBackup); + const ruleForBackup = new events.EventRule(this, 'RuleForBackup', { + scheduleExpression: scheduleForBackup, + }); + ruleForBackup.addTarget(enumerateBoardsFunction); } createEnumerateBoardsFunction(backupBoardTopic : Topic, monitoringTopic : Topic) : lambda.Function { @@ -70,13 +73,6 @@ class TrelloBackupStack extends cdk.Stack { return lambdaFunction; } - schedule(lambdaFunction : lambda.Function, scheduleExpression : string) : void { - const rule = new events.EventRule(this, 'ScheduleExpressionRule', { - scheduleExpression: scheduleExpression, - }); - rule.addTarget(lambdaFunction); - } - reportErrors(lambdaFunction : lambda.Function, monitoringTopic : Topic) : void { const metricErrors = lambdaFunction.metricErrors(); const alarm = metricErrors.newAlarm(this, `${lambdaFunction.id}Alarm`, { From d634b0d3b861a3d920a276b2acdd51ef248c0577 Mon Sep 17 00:00:00 2001 From: James Mead <3169+floehopper@users.noreply.github.com> Date: Sat, 29 Dec 2018 14:36:58 +0000 Subject: [PATCH 57/57] Add post-backup check Sends an email indicating whether the backup of all boards was successful or not. --- README.md | 2 + index.ts | 25 ++++++++ lambdaFunctions/checkBoardBackups/.gitignore | 2 + lambdaFunctions/checkBoardBackups/Gemfile | 7 +++ .../checkBoardBackups/Gemfile.lock | 37 +++++++++++ lambdaFunctions/checkBoardBackups/build.sh | 5 ++ lambdaFunctions/checkBoardBackups/index.rb | 62 +++++++++++++++++++ 7 files changed, 140 insertions(+) create mode 100644 lambdaFunctions/checkBoardBackups/.gitignore create mode 100644 lambdaFunctions/checkBoardBackups/Gemfile create mode 100644 lambdaFunctions/checkBoardBackups/Gemfile.lock create mode 100755 lambdaFunctions/checkBoardBackups/build.sh create mode 100644 lambdaFunctions/checkBoardBackups/index.rb diff --git a/README.md b/README.md index 2003453..3d27561 100644 --- a/README.md +++ b/README.md @@ -54,6 +54,8 @@ Ensure you're logged in as the GFR Admin user on https://trello.com. It's safe t In the top-level `.env` file, set the following environment variables: * `TRELLO_BACKUP_SCHEDULE_FOR_BACKUP` - specifies how often the backup is performed, e.g. `cron(0 2 * * ? *)` runs daily at 2am (see [Schedule Expressions for Rules][3] for details) +* `TRELLO_BACKUP_SCHEDULE_FOR_CHECK` - specifies how often the post-backup check is performed, e.g. `cron(30 2 * * ? *)` runs daily at 2.30am +* `TRELLO_BACKUP_OLDEST_ALLOWED_BACKUP_IN_SECONDS` - how old a backup is allowed to be when the post-backup check is performed, e.g. `1800` only allows backups to be 30 minutes old; older backups trigger an error * `TRELLO_BACKUP_S3_BUCKET_NAME` - the name of the S3 bucket you want to be created and used to save backups * `TRELLO_BACKUP_MONITORING_EMAIL_ADDRESS` - the email address where monitoring emails will be sent (an email will be sent on first deployment to ask you to confirm the subscription) diff --git a/index.ts b/index.ts index 1455b05..baab4c3 100644 --- a/index.ts +++ b/index.ts @@ -29,6 +29,9 @@ class TrelloBackupStack extends cdk.Stack { backupBoardTopic.subscribeLambda(backupBoardFunction); + const checkBoardBackupsFunction + = this.createCheckBoardBackupsFunction(boardBackupsBucket, monitoringTopic); + const monitoringEmailAddress = process.env.TRELLO_BACKUP_MONITORING_EMAIL_ADDRESS; monitoringTopic.subscribeEmail('monitoringTopicEmail', monitoringEmailAddress); @@ -37,6 +40,12 @@ class TrelloBackupStack extends cdk.Stack { scheduleExpression: scheduleForBackup, }); ruleForBackup.addTarget(enumerateBoardsFunction); + + const scheduleForCheck = process.env.TRELLO_BACKUP_SCHEDULE_FOR_CHECK; + const ruleForCheck = new events.EventRule(this, 'RuleForCheck', { + scheduleExpression: scheduleForCheck, + }); + ruleForCheck.addTarget(checkBoardBackupsFunction); } createEnumerateBoardsFunction(backupBoardTopic : Topic, monitoringTopic : Topic) : lambda.Function { @@ -73,6 +82,22 @@ class TrelloBackupStack extends cdk.Stack { return lambdaFunction; } + createCheckBoardBackupsFunction(boardBackupsBucket : s3.Bucket, monitoringTopic : Topic) : lambda.Function { + const lambdaFunction = new lambda.Function(this, 'checkBoardBackups', { + runtime: rubyLambdaRuntime, + handler: 'index.handler', + code: lambda.Code.asset('./lambdaFunctions/checkBoardBackups'), + environment: { + TRELLO_BACKUP_MONITORING_TOPIC_ARN: monitoringTopic.topicArn + }, + timeout: lambdaFunctionTimeout + }); + boardBackupsBucket.grantRead(lambdaFunction.role); + monitoringTopic.grantPublish(lambdaFunction.role); + this.reportErrors(lambdaFunction, monitoringTopic); + return lambdaFunction; + } + reportErrors(lambdaFunction : lambda.Function, monitoringTopic : Topic) : void { const metricErrors = lambdaFunction.metricErrors(); const alarm = metricErrors.newAlarm(this, `${lambdaFunction.id}Alarm`, { diff --git a/lambdaFunctions/checkBoardBackups/.gitignore b/lambdaFunctions/checkBoardBackups/.gitignore new file mode 100644 index 0000000..226ca36 --- /dev/null +++ b/lambdaFunctions/checkBoardBackups/.gitignore @@ -0,0 +1,2 @@ +.env +vendor diff --git a/lambdaFunctions/checkBoardBackups/Gemfile b/lambdaFunctions/checkBoardBackups/Gemfile new file mode 100644 index 0000000..24c9cce --- /dev/null +++ b/lambdaFunctions/checkBoardBackups/Gemfile @@ -0,0 +1,7 @@ +source 'https://rubygems.org' + +ruby '~> 2.5.0' + +gem 'aws-sdk-s3' +gem 'aws-sdk-sns' +gem 'dotenv' diff --git a/lambdaFunctions/checkBoardBackups/Gemfile.lock b/lambdaFunctions/checkBoardBackups/Gemfile.lock new file mode 100644 index 0000000..c789184 --- /dev/null +++ b/lambdaFunctions/checkBoardBackups/Gemfile.lock @@ -0,0 +1,37 @@ +GEM + remote: https://rubygems.org/ + specs: + aws-eventstream (1.0.1) + aws-partitions (1.127.0) + aws-sdk-core (3.44.1) + aws-eventstream (~> 1.0) + aws-partitions (~> 1.0) + aws-sigv4 (~> 1.0) + jmespath (~> 1.0) + aws-sdk-kms (1.13.0) + aws-sdk-core (~> 3, >= 3.39.0) + aws-sigv4 (~> 1.0) + aws-sdk-s3 (1.30.0) + aws-sdk-core (~> 3, >= 3.39.0) + aws-sdk-kms (~> 1) + aws-sigv4 (~> 1.0) + aws-sdk-sns (1.9.0) + aws-sdk-core (~> 3, >= 3.39.0) + aws-sigv4 (~> 1.0) + aws-sigv4 (1.0.3) + dotenv (2.5.0) + jmespath (1.4.0) + +PLATFORMS + ruby + +DEPENDENCIES + aws-sdk-s3 + aws-sdk-sns + dotenv + +RUBY VERSION + ruby 2.5.3p105 + +BUNDLED WITH + 1.17.1 diff --git a/lambdaFunctions/checkBoardBackups/build.sh b/lambdaFunctions/checkBoardBackups/build.sh new file mode 100755 index 0000000..9c37336 --- /dev/null +++ b/lambdaFunctions/checkBoardBackups/build.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash +set -e +cp ../../.env . +rm -rf vendor/bundle +bundle install --path vendor/bundle diff --git a/lambdaFunctions/checkBoardBackups/index.rb b/lambdaFunctions/checkBoardBackups/index.rb new file mode 100644 index 0000000..51a930f --- /dev/null +++ b/lambdaFunctions/checkBoardBackups/index.rb @@ -0,0 +1,62 @@ +require 'aws-sdk-s3' +require 'aws-sdk-sns' +require 'csv' +require 'dotenv' + +Dotenv.load + +TOPIC_ARN = ENV.fetch('TRELLO_BACKUP_MONITORING_TOPIC_ARN') +S3_BUCKET_NAME = ENV.fetch('TRELLO_BACKUP_S3_BUCKET_NAME') +OLDEST_ALLOWED_BACKUP_IN_SECONDS = Integer(ENV.fetch('TRELLO_BACKUP_OLDEST_ALLOWED_BACKUP_IN_SECONDS')) + +def handler(event:, context:) + endpoint = "https://api.trello.com/1/members/me/boards" + uri = URI(endpoint) + uri.query = URI.encode_www_form({ + :key => ENV.fetch('TRELLO_KEY'), + :token => ENV.fetch('TRELLO_TOKEN'), + :fields => 'id,name' + }) + response = Net::HTTP.get_response(uri) + unless response.is_a?(Net::HTTPSuccess) + raise "Trello API error: #{response.message} #{response.code}" + end + boards = JSON.parse(response.body) + + earliest_allowed_backup_time = Time.now - OLDEST_ALLOWED_BACKUP_IN_SECONDS + boards_with_status = boards.map do |board| + board_id = board['id'] + object_key = "#{board_id}.json" + result = { id: board_id, name: board['name'] } + begin + object = Aws::S3::Object.new(S3_BUCKET_NAME, object_key) + result[:message] = "Last backed up: #{object.data.last_modified}" + if object.data.last_modified >= earliest_allowed_backup_time + result.merge(success: true) + else + result.merge(success: false) + end + rescue Aws::S3::Errors::NotFound => e + result.merge(success: false, message: e.inspect) + end + end + + success = boards_with_status.all? { |b| b[:success] } + subject = "Trello Backup: #{success ? 'Success' : 'Failure'}" + message = '' + CSV(message, col_sep: "\t") do |csv| + csv << %w[id name success message] + boards_with_status.each do |board| + csv << [board[:id], board[:name], board[:success], board[:message]] + end + end + + sns = Aws::SNS::Client.new + sns.publish( + topic_arn: TOPIC_ARN, + subject: subject, + message: message, + ) + + { statusCode: 200, body: 'OK' } +end