brillo 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: b684f9b150cde1eded3db5ae684ca02853e6ae37
4
+ data.tar.gz: 1a052b7afc30957c7b87b04506be442c511b65ea
5
+ SHA512:
6
+ metadata.gz: b1726eac1715f1fda25a19e35f77e82bdfe528378c83736ca194ff86c0a1d6bd57eb2d203d4583d8e0cef9e758209200b3d818d167e3533f13d013b1ca0f8fb9
7
+ data.tar.gz: 2e9857f15ea8486806208bf64954b35eec1371fceb183b27803e828baf1d6726a718f350f50f91ffe6f23e230f80270d8529820db706dfd9a4f5f7ed62055fed
data/.gitignore ADDED
@@ -0,0 +1,11 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /spec/examples.txt
10
+ /tmp/
11
+ /*.gem
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --require spec_helper
data/.travis.yml ADDED
@@ -0,0 +1,4 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.1.5
4
+ before_install: gem install bundler -v 1.10.6
data/CHANGELOG.md ADDED
@@ -0,0 +1,6 @@
1
+ # Change Log
2
+
3
+ ## 1.0.0
4
+ First public Brillo version!
5
+
6
+ ## 0.3.0
data/Gemfile ADDED
@@ -0,0 +1,6 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in brillo.gemspec
4
+ gemspec
5
+
6
+ gem 'rails', "~> 4.2.0"
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2016 Matt Bessey
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,104 @@
1
+ [![Build Status](https://travis-ci.com/bessey/brillo.svg?token=z16y9ppDyNfaLAvjjHbK&branch=master)](https://travis-ci.com/bessey/brillo)
2
+
3
+ # Brillo
4
+
5
+ Brillo is a Rails database scrubber and loader, useful for making lightweight copies of your production database for development machines, with sensitive information obfuscated. Most configuration is done through YAML: Specify the models that you want to back up, what associations you want with them, and what fields should be obfuscated (and how).
6
+
7
+ Once that is done, dropping your local DB and replacing it with the latest scrubbed copy is as easy as `rake db:load`.
8
+
9
+ Under the hood we use [Polo](https://github.com/IFTTT/polo) to explore the classes and associations you specify in brillo.yml, obfuscated fields as configured.
10
+
11
+ ## Installation
12
+
13
+ Add this line to your application's Gemfile:
14
+
15
+ ```ruby
16
+ gem 'brillo'
17
+ # We currently rely on an unreleased version of Polo
18
+ gem 'polo', github: 'IFTTT/polo'
19
+ ```
20
+
21
+ Generate a starter `brillo.yml` file and `config/initializers/brillo.rb` with
22
+
23
+ ```bash
24
+ $ rails g brillo_config
25
+ ```
26
+
27
+ If you're using Capistrano, add Brillo's tasks to your Capfile:
28
+
29
+ ```ruby
30
+ # Capfile
31
+ require 'capistrano/brillo'
32
+ ```
33
+
34
+ Lastly, since the scrubber is pretty resource intensive you may wish to ensure it runs on separate hardware from your app servers:
35
+
36
+ ```ruby
37
+ # config/deploy.rb
38
+ set :brillo_role, :my_batch_role
39
+ ```
40
+
41
+ ## Usage
42
+
43
+ Here's an example `brillo.yml` for IMDB:
44
+
45
+ ```yaml
46
+ name: imdb # Namespace the scrubbed file will occupy in S3
47
+ explore:
48
+ user: # Name of ActiveRecord class in snake_case
49
+ tactic: all # Scrubbing tactic to use (see Brillo:TACTICS for choices)
50
+ associations: # Associations to include in the scrub (ALL associated records included)
51
+ - comments
52
+ movie:
53
+ tactic: latest # The latest tactic explores the most recent 1,000 records
54
+ associations:
55
+ - actors
56
+ - ratings
57
+ admin/note: # Corresponds to the Admin::Note class
58
+ tactic: all
59
+ obfuscations: #
60
+ user.name: name # Scrub user.name with the "name" scrubber (see Brillo::SCRUBBERS for choices)
61
+ user.phone: phone
62
+ user.email: email
63
+ ```
64
+
65
+ In order to communicate with S3, Brillo expects `AWS_ACCESS_KEY` and `AWS_SECRET_KEY` to be set in the environment. It uses [Tim Kay's AWS cli](http://timkay.com/aws/) to communicate with AWS.
66
+
67
+ ### Loading a database in development
68
+
69
+ ```bash
70
+ $ rake db:load
71
+ ```
72
+
73
+ ### Loading a database on a stage
74
+
75
+ ```bash
76
+ $ cap staging db:load
77
+ ```
78
+
79
+ ### Adding scrub tactics and obfuscations
80
+
81
+ If the built in record selection tactics aren't enough for you, or you need a custom obfuscation strategy, you can add them via the initializer. They are available in the YAML config like any other strategy.
82
+
83
+ ```ruby
84
+ # config/initializers/brillo.rb
85
+
86
+ Brillo.configure do |config|
87
+ config.add_tactic :oldest, -> (klass) { klass.order(created_at: :desc).limit(1000) }
88
+
89
+ config.add_obfuscation :remove_ls, -> (field) {
90
+ field.gsub(/l/, "X")
91
+ }
92
+
93
+ # If you need the context of the entire record being obfuscated, it is available in the second argument
94
+ config.add_obfuscation :phone_with_id, -> (field, instance) {
95
+ (555_000_0000 + instance.id).to_s
96
+ }
97
+ end
98
+
99
+ ```
100
+
101
+ ## To Do
102
+
103
+ - Support S3 transfer via the usual AWS CLI
104
+ - Support alternative transfer mechanisms
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
data/bin/console ADDED
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "rails"
5
+ require "brillo"
6
+
7
+ # You can add fixtures and/or initialization code here to make experimenting
8
+ # with your gem easier. You can also use a different console, if you like.
9
+
10
+ # (If you use this, don't forget to add pry to your Gemfile!)
11
+ # require "pry"
12
+ # Pry.start
13
+
14
+ require "irb"
15
+ IRB.start
data/bin/setup ADDED
@@ -0,0 +1,7 @@
1
+ #!/bin/bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+
5
+ bundle install
6
+
7
+ # Do any other automated setup that you need to do here
data/brillo.gemspec ADDED
@@ -0,0 +1,30 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'brillo/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "brillo"
8
+ spec.version = Brillo::VERSION
9
+ spec.authors = ["Matt Bessey"]
10
+ spec.email = ["mbessey@caring.com"]
11
+
12
+ spec.summary = %q{Rails database scrubber and loader, great for seeding your dev db with real but sanitized data}
13
+ spec.homepage = "https://github.com/bessey/brillo"
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features|dummy)/}) }
17
+ spec.bindir = "exe"
18
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_runtime_dependency "rake", "~> 10.0"
22
+ spec.add_runtime_dependency "capistrano", "~> 3.0"
23
+ spec.add_runtime_dependency "polo", "~> 0.3"
24
+
25
+ spec.add_development_dependency "rails", ">= 3.2"
26
+ spec.add_development_dependency "rspec", "~> 3.4"
27
+ spec.add_development_dependency "pry"
28
+ spec.add_development_dependency "benchmark-ips"
29
+ spec.add_development_dependency "geminabox"
30
+ end
@@ -0,0 +1,13 @@
1
+ name: my-app # Namespace the scrubbed file will occupy
2
+ ## Optional configuration (with defaults shown)
3
+ # compress: true # Enables gzip on scrub and ungzip on load
4
+ # send_to_s3: true # Disable to skip sending to S3, useful for debugging
5
+ # fetch_from_s3: true # Disable to skip fetching from S3, useful when you have an existing scrub in tmp/
6
+ #
7
+ # obfuscations:
8
+ # user.name: name # Scrub table.field with scrubber (see Brillo::SCRUBBERS for choices)
9
+ explore:
10
+ user: # Name of ActiveRecord class in snake_case
11
+ tactic: all # Scrubbing tactic to use (see Brillo:TACTICS for choices)
12
+ associations: # Associations to include in the scrub (ALL associated records included)
13
+ - comments
@@ -0,0 +1,14 @@
1
+ Brillo.configure do |config|
2
+ ## Add extra tactics for selecting records as you need them
3
+ # config.add_tactic :oldest, -> (klass) { klass.order(created_at: :desc).limit(1000) }
4
+
5
+ ## Custom obfuscations can also be added
6
+ # config.add_obfuscation :remove_ls, -> (field) {
7
+ # field.gsub(/l/, "X")
8
+ # }
9
+
10
+ ## If you need the context of the entire record being obfuscated, it is available in the second argument
11
+ # config.add_obfuscation :phone_with_id, -> (field, instance) {
12
+ # (555_000_0000 + instance.id).to_s
13
+ # }
14
+ end
@@ -0,0 +1,3 @@
1
+ ## Brillo::Adapter
2
+
3
+ Adapter allow database specific setup and teardown to be added to the Scrubber's dumped SQL.
@@ -0,0 +1,27 @@
1
+ module Brillo
2
+ module Adapter
3
+ class Base
4
+ attr_reader :config
5
+ def initialize(db_config)
6
+ @config = db_config
7
+ end
8
+ def header
9
+ ActiveRecord::Base.connection.dump_schema_information
10
+ end
11
+
12
+ def footer
13
+ ""
14
+ end
15
+
16
+ def dump_structure_and_migrations(config)
17
+ # Overrides the path the structure is dumped to in Rails >= 3.2
18
+ ENV['SCHEMA'] = ENV['DB_STRUCTURE'] = config.dump_path.to_s
19
+ Rake::Task["db:structure:dump"].invoke
20
+ end
21
+
22
+ def load_command
23
+ raise NotImplementedError
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,32 @@
1
+ module Brillo
2
+ module Adapter
3
+ class MySQL < Base
4
+ def header
5
+ super + <<-SQL
6
+ -- Disable autocommit, uniquechecks, and foreign key checks, for performance on InnoDB
7
+ -- http://dev.mysql.com/doc/refman/5.5/en/optimizing-innodb-bulk-data-loading.html
8
+ SET @OLD_AUTOCOMMIT=@@AUTOCOMMIT, AUTOCOMMIT = 0;
9
+ SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS = 0;
10
+ SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS = 0;
11
+ SQL
12
+ end
13
+
14
+ def footer
15
+ super + <<-SQL
16
+ SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS;
17
+ SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS;
18
+ SET AUTOCOMMIT = @OLD_AUTOCOMMIT;
19
+ COMMIT;
20
+ SQL
21
+ end
22
+
23
+ def dump_structure_and_migrations(config)
24
+ Dumper::MysqlDumper.new(config).dump
25
+ end
26
+
27
+ def load_command
28
+ "mysql --host #{config[:host]} -u #{config[:username]} #{config[:password] ? "-p#{config[:password]}" : ""} #{config[:database]}"
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,9 @@
1
+ module Brillo
2
+ module Adapter
3
+ class Postgres < Base
4
+ def load_command
5
+ "psql --host #{config[:host]} -U #{config[:username]} #{config[:password] ? "-W#{config[:password]}" : ""} #{config[:database]}"
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,92 @@
1
+ module Brillo
2
+ class Config
3
+ AWS_KEY_PATH = '/etc/ec2_secure_env.yml'
4
+ S3_BUCKET = 'scrubbed_databases2'
5
+ attr_reader :app_name, :compress, :obfuscations, :klass_association_map, :db, :send_to_s3, :fetch_from_s3,
6
+ :aws_key_path, :s3_bucket
7
+
8
+ def initialize(options = {})
9
+ @app_name = options.fetch("name")
10
+ @klass_association_map = options["explore"] || {}
11
+ @compress = options.fetch("compress", true)
12
+ @fetch_from_s3 = options.fetch("fetch_from_s3", true)
13
+ @send_to_s3 = options.fetch("send_to_s3", true)
14
+ @aws_key_path = options.fetch("aws_key_path", AWS_KEY_PATH)
15
+ @s3_bucket = options.fetch("s3_bucket", S3_BUCKET)
16
+ @obfuscations = parse_obfuscations(options["obfuscations"] || {})
17
+ rescue KeyError => e
18
+ raise ConfigParseError, e
19
+ end
20
+
21
+ def verify!
22
+ @obfuscations.each do |field, strategy|
23
+ next if Scrubber::SCRUBBERS[strategy]
24
+ raise ConfigParseError, "Scrub strategy '#{strategy}' not found, but required by '#{field}'"
25
+ end
26
+ @klass_association_map.each do |klass, _|
27
+ next if klass.camelize.safe_constantize
28
+ raise ConfigParseError, "Class #{klass} not found"
29
+ end
30
+ self
31
+ end
32
+
33
+ def add_obfuscation(name, scrubber)
34
+ Scrubber::SCRUBBERS[name] = scrubber
35
+ end
36
+
37
+ def add_tactic(name, tactic)
38
+ Scrubber::TACTICS[name] = tactic
39
+ end
40
+
41
+ def app_tmp
42
+ Rails.root.join "tmp"
43
+ end
44
+
45
+ def dump_filename
46
+ "#{app_name}-scrubbed.dmp"
47
+ end
48
+
49
+ def remote_filename
50
+ compress ? "#{dump_filename}.gz" : dump_filename
51
+ end
52
+
53
+ def dump_path
54
+ app_tmp + dump_filename
55
+ end
56
+
57
+ def remote_path
58
+ app_tmp + remote_filename
59
+ end
60
+
61
+ def db
62
+ @db_config ||= ActiveRecord::Base.connection.instance_variable_get(:@config).dup
63
+ end
64
+
65
+ # TODO support other tranfer systems
66
+ def transferrer
67
+ Transferrer::S3.new(self)
68
+ end
69
+
70
+ def adapter
71
+ case db[:adapter].to_sym
72
+ when :mysql2
73
+ Adapter::MySQL.new(db)
74
+ when :postgres
75
+ Adapter::Postgres.new(db)
76
+ else
77
+ raise ConfigParseError, "Unsupported DB adapter #{db[:adapter]}"
78
+ end
79
+ end
80
+
81
+ # Convert generic cross table obfuscations to symbols so Polo parses them correctly
82
+ # "my_table.field" => "my_table.field"
83
+ # "my_field" => :my_field
84
+ def parse_obfuscations(obfuscations)
85
+ obfuscations.each_pair.with_object({}) do |field_and_strategy, hash|
86
+ field, strategy = field_and_strategy
87
+ strategy = strategy.to_sym
88
+ field.match(/\./) ? hash[field] = strategy : hash[field.to_sym] = strategy
89
+ end
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,44 @@
1
+ module Brillo
2
+ module Dumper
3
+ class MysqlDumper
4
+ include Helpers::ExecHelper
5
+ include Logger
6
+ attr_reader :config
7
+ def initialize(config)
8
+ @config = config
9
+ end
10
+
11
+ def dump
12
+ db = config.db
13
+ execute!(
14
+ "mysqldump",
15
+ host_arg,
16
+ "-u #{db[:username]}",
17
+ password_arg,
18
+ "--no-data",
19
+ "--single-transaction", # InnoDB only. Prevent MySQL locking the whole database during dump.
20
+ "#{db[:database]}",
21
+ "> #{config.dump_path}"
22
+ )
23
+ end
24
+
25
+ private
26
+
27
+ def password_arg
28
+ if password = config.db[:password].presence
29
+ "--password=#{password}"
30
+ else
31
+ ""
32
+ end
33
+ end
34
+
35
+ def host_arg
36
+ if (host = config.db[:host].presence) && host != 'localhost'
37
+ "-h #{host}"
38
+ else
39
+ ""
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,4 @@
1
+ module Brillo
2
+ ConfigParseError = Class.new(StandardError)
3
+ CredentialsError = Class.new(StandardError)
4
+ end
@@ -0,0 +1,31 @@
1
+ require 'open3'
2
+ module Brillo
3
+ module Helpers
4
+ module ExecHelper
5
+ def execute *command
6
+ command_string = Array(command).join(' ')
7
+ log_anonymized command_string
8
+ stdout, stderr, status = Open3.capture3 command_string
9
+ [status.success?, stdout, stderr]
10
+ end
11
+
12
+ def execute! *command
13
+ success, stdout, stderr = execute(command)
14
+ if success
15
+ [success, stdout, stderr]
16
+ else
17
+ raise RuntimeError, stderr
18
+ end
19
+ end
20
+
21
+ private
22
+
23
+ def log_anonymized command_string
24
+ command_string = command_string.gsub(/--password=[^\s]+/, "--password={FILTERED}")
25
+ command_string = command_string.gsub(/EC2_ACCESS_KEY=[^\s]+/, "EC2_ACCESS_KEY={FILTERED}")
26
+ command_string = command_string.gsub(/EC2_SECRET_KEY=[^\s]+/, "EC2_SECRET_KEY={FILTERED}")
27
+ logger.info "Running \n\t #{command_string}"
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,42 @@
1
+ module Brillo
2
+ # Responsible for fetching an existing SQL scrub from S3, cleaning the database,
3
+ # and loading the SQL.
4
+ class Loader
5
+ include Helpers::ExecHelper
6
+ include Logger
7
+ attr_reader :config
8
+
9
+ def initialize(config)
10
+ raise "⚠️ DON'T LOAD IN PRODUCTION! ⚠️" if Rails.env.production?
11
+ @config = config
12
+ end
13
+
14
+ def load!
15
+ config.transferrer.download
16
+ recreate_db
17
+ import_sql
18
+ end
19
+
20
+ def recreate_db
21
+ ["db:drop", "db:create"].each do |t|
22
+ logger.info "Running\n\trake #{t}"
23
+ Rake::Task[t].invoke
24
+ end
25
+ end
26
+
27
+ def import_sql
28
+ if config.compress
29
+ execute!("gunzip -c #{config.remote_path} | #{sql_load_command}")
30
+ else
31
+ execute!("cat #{config.dump_path} | #{sql_load_command}")
32
+ end
33
+ logger.info "Import complete!"
34
+ end
35
+
36
+ private
37
+
38
+ def sql_load_command
39
+ config.adapter.load_command
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,15 @@
1
+ module Brillo
2
+ module Logger
3
+ def self.logger= logger
4
+ @logger = logger
5
+ end
6
+
7
+ def self.logger
8
+ @logger ||= Rails.logger
9
+ end
10
+
11
+ def logger
12
+ Brillo::Logger.logger
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,10 @@
1
+ module Brillo
2
+ class Railtie < Rails::Railtie
3
+ rake_tasks do
4
+ load "tasks/brillo.rake"
5
+ end
6
+ generators do
7
+ require "generators/brillo.rb"
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,111 @@
1
+ module Brillo
2
+ # Responsible for creating a fresh scrubbed SQL copy of the database,
3
+ # as specified via config, and uploading to S3
4
+ class Scrubber
5
+ include Helpers::ExecHelper
6
+ include Logger
7
+ JUMBLE_PRNG = Random.new
8
+ LATEST_LIMIT = 1_000
9
+
10
+ # Define some procs as scrubbing strategies for Polo
11
+ SCRUBBERS = {
12
+ default_time: ->(t) { t.nil? ? Time.now.to_s(:sql) : t },
13
+ email: ->(e) { e.match(/@caring.com/) ? e : Digest::MD5.hexdigest(e) + "@example.com".freeze },
14
+ jumble: ->(j) { j.downcase.chars.shuffle!(random: JUMBLE_PRNG.clone).join },
15
+ phone: ->(n) { n = n.split(' ').first; n && n.length > 9 ? n[0..-5] + n[-1] + n[-2] + n[-3] + n[-4] : n}, # strips extensions
16
+ name: ->(n) { n.downcase.split(' ').map do |word|
17
+ word.chars.shuffle!(random: JUMBLE_PRNG.clone).join
18
+ end.each(&:capitalize!).join(' ')
19
+ },
20
+ }
21
+
22
+ TACTICS = {
23
+ latest: -> (klass) { klass.order('id desc').limit(LATEST_LIMIT).pluck(:id) },
24
+ all: -> (klass) { klass.pluck(:id) }
25
+ }
26
+
27
+ attr_reader :config, :adapter, :transferrer
28
+
29
+ def initialize(config)
30
+ @config = config
31
+ @adapter = config.adapter
32
+ end
33
+
34
+ def scrub!
35
+ FileUtils.rm [config.dump_path, config.remote_path], force: true
36
+ configure_polo
37
+ adapter.dump_structure_and_migrations(config)
38
+ explore_all_classes
39
+ compress
40
+ config.transferrer.upload
41
+ end
42
+
43
+ def explore_all_classes
44
+ File.open(config.dump_path, "a") do |sql_file|
45
+ sql_file.puts(adapter.header)
46
+ klass_association_map.each do |klass, options|
47
+ begin
48
+ klass = deserialize_class(klass)
49
+ tactic = deserialize_tactic(options)
50
+ rescue ConfigParseError => e
51
+ logger.error "Error in brillo.yml: #{e.message}"
52
+ next
53
+ end
54
+ associations = options.fetch("associations", [])
55
+ explore_class(klass, tactic, associations) do |insert|
56
+ sql_file.puts(insert)
57
+ end
58
+ end
59
+ sql_file.puts(adapter.footer)
60
+ end
61
+ end
62
+
63
+ private
64
+
65
+ def compress
66
+ return unless config.compress
67
+ execute!("gzip -f #{config.dump_path}")
68
+ end
69
+
70
+ def explore_class(klass, tactic_or_ids, associations)
71
+ ids = tactic_or_ids.is_a?(Symbol) ? TACTICS.fetch(tactic_or_ids).call(klass) : tactic_or_ids
72
+ logger.info("Scrubbing #{ids.length} #{klass} rows with associations #{associations}")
73
+ Polo.explore(klass, ids, associations).each do |row|
74
+ yield "#{row};"
75
+ end
76
+ end
77
+
78
+ def klass_association_map
79
+ config.klass_association_map
80
+ end
81
+
82
+ def obfuscations
83
+ config.obfuscations.map do |field, strategy|
84
+ [field, SCRUBBERS.fetch(strategy)]
85
+ end.to_h
86
+ end
87
+
88
+ def configure_polo
89
+ obfs = obfuscations
90
+ adapter = config.db[:adapter]
91
+ Polo.configure do
92
+ obfuscate obfs
93
+ if adapter == "mysql2"
94
+ on_duplicate :ignore
95
+ end
96
+ end
97
+ end
98
+
99
+ def deserialize_class(klass)
100
+ klass.camelize.constantize
101
+ rescue
102
+ raise Config::ConfigParseError, "Could not process class '#{klass}'"
103
+ end
104
+
105
+ def deserialize_tactic(options)
106
+ options.fetch("tactic").to_sym
107
+ rescue KeyError
108
+ raise ConfigParseError, "Tactic not specified for class #{klass}"
109
+ end
110
+ end
111
+ end
@@ -0,0 +1,66 @@
1
+ module Brillo
2
+ module Transferrer
3
+ class S3
4
+ include Helpers::ExecHelper
5
+ include Logger
6
+ attr_reader :credentials, :bucket, :remote_filename, :remote_path, :download_enabled, :upload_enabled
7
+ attr_reader :key_path
8
+
9
+ def initialize(config)
10
+ @download_enabled = config.fetch_from_s3
11
+ @upload_enabled = config.send_to_s3
12
+ @bucket = config.s3_bucket
13
+ @key_path = config.aws_key_path
14
+ @remote_filename = config.remote_filename
15
+ @remote_path = config.remote_path
16
+ load_credentials
17
+ end
18
+
19
+ def download
20
+ return unless download_enabled
21
+ load_credentials
22
+ FileUtils.rm [config.dump_path, config.remote_path], force: true
23
+ aws_s3 "get"
24
+ end
25
+
26
+ def upload
27
+ return unless upload_enabled
28
+ load_credentials
29
+ aws_s3 "put"
30
+ end
31
+
32
+ private
33
+
34
+ def load_credentials
35
+ if File.exist?(key_path)
36
+ @credentials = YAML.load_file(key_path)
37
+ else
38
+ key = ENV["AWS_SECRET_KEY"] || ENV["EC2_SECRET_KEY"]
39
+ unless key && key.length > 10
40
+ raise CredentialsError, "AWS credentials not found. Expected AWS_ACCESS_KEY and AWS_SECRET_KEY to be set!"
41
+ end
42
+ @credentials = {
43
+ 'aws_access_key' => ENV["AWS_ACCESS_KEY"] || ENV["EC2_ACCESS_KEY"],
44
+ 'aws_secret_key' => key
45
+ }
46
+ end
47
+ end
48
+
49
+ def aws_s3 api_command
50
+ execute!("#{aws_env} #{aws_bin} #{api_command} #{bucket}/#{remote_filename} #{remote_path}")
51
+ end
52
+
53
+ def aws_bin
54
+ if File.exist?('/usr/local/bin/awstk')
55
+ '/usr/local/bin/awstk'
56
+ else
57
+ '/usr/local/bin/aws'
58
+ end
59
+ end
60
+
61
+ def aws_env
62
+ "EC2_ACCESS_KEY=#{credentials['aws_access_key']} EC2_SECRET_KEY=#{credentials['aws_secret_key']}"
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,32 @@
1
+ module Brillo
2
+ # Responsible for asserting that the config file is valid
3
+ class Scrubber
4
+ include Common
5
+
6
+ attr_reader :config
7
+
8
+ def initialize(config)
9
+ parse_config(config)
10
+ end
11
+
12
+ def validate
13
+ errors = Hash.new({}.freeze)
14
+ klass_association_map.each do |klass, options|
15
+ begin
16
+ real_klass = deserialize_class(klass)
17
+ rescue
18
+ errors[klass][:name] = "No such class #{klass.camelize}, did you use the singular?"
19
+ end
20
+ begin
21
+ tactic = options.fetch("tactic").to_sym
22
+ rescue KeyError
23
+ errors[klass][:tactic] "Tactic not specified"
24
+ end
25
+ end
26
+ end
27
+
28
+ def deserialize_class(klass)
29
+ klass.camelize.constantize
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,3 @@
1
+ module Brillo
2
+ VERSION = "1.0.0"
3
+ end
data/lib/brillo.rb ADDED
@@ -0,0 +1,50 @@
1
+ require "brillo/version"
2
+
3
+ require 'brillo/errors'
4
+ require 'brillo/helpers/exec_helper'
5
+ require 'brillo/logger'
6
+
7
+ require 'brillo/adapter/base'
8
+ require 'brillo/adapter/mysql'
9
+ require 'brillo/adapter/postgres'
10
+
11
+ require 'brillo/transferrer/s3'
12
+
13
+ require 'brillo/dumper/mysql_dumper'
14
+ require 'brillo/railtie'
15
+ require 'brillo/config'
16
+ require 'brillo/scrubber'
17
+ require 'brillo/loader'
18
+ require 'polo'
19
+
20
+ module Brillo
21
+ def self.configure
22
+ yield config
23
+ begin
24
+ config.verify!
25
+ rescue ConfigParseError => e
26
+ puts "Brillo config contains errors: #{e}"
27
+ end
28
+ end
29
+
30
+ def self.scrub!(logger: ::Logger.new(STDOUT))
31
+ Brillo::Logger.logger = logger
32
+ Scrubber.new(config).scrub!
33
+ end
34
+
35
+ def self.load!(logger: ::Logger.new(STDOUT))
36
+ Brillo::Logger.logger = logger
37
+ Loader.new(config).load!
38
+ end
39
+
40
+ def self.config
41
+ @config ||= begin
42
+ static_config = YAML.load_file("#{Rails.root.to_s}/config/brillo.yml")
43
+ Config.new(static_config)
44
+ end
45
+ end
46
+
47
+ def self.config=(config)
48
+ @config = config
49
+ end
50
+ end
@@ -0,0 +1 @@
1
+ load File.expand_path('../tasks/db.cap', __FILE__)
@@ -0,0 +1,30 @@
1
+ namespace :db do
2
+ desc "Run the DB scrubber and push to S3"
3
+ task :scrub do
4
+ on primary(fetch(:brillo_role)) do
5
+ with rails_env: fetch(:stage) do
6
+ within current_path do
7
+ execute :rake, 'db:scrub'
8
+ end
9
+ end
10
+ end
11
+ end
12
+
13
+ desc "Run the S3 scrubbed DB loader"
14
+ task :load do
15
+ raise "Don't you dare!" if fetch(:stage) == "production"
16
+ on release_roles(:all).sample do
17
+ with rails_env: fetch(:stage) do
18
+ within current_path do
19
+ execute :rake, 'db:load'
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
25
+
26
+ namespace :load do
27
+ task :defaults do
28
+ set :brillo_role, :batch
29
+ end
30
+ end
@@ -0,0 +1,9 @@
1
+ class BrilloConfigGenerator < Rails::Generators::Base
2
+ source_root File.expand_path("../../../", __FILE__)
3
+
4
+ desc "Create a plain Brillo YAML configuration and initializer"
5
+ def create_initializer_file
6
+ copy_file "config/brillo-example.yml", "config/brillo.yml"
7
+ copy_file "config/brillo-initializer.rb", "config/initializers/brillo.rb"
8
+ end
9
+ end
@@ -0,0 +1,26 @@
1
+ require 'yaml'
2
+
3
+ namespace :db do
4
+ desc 'Upload a scrubbed copy of the database as specified by config/scrub.yml to S3'
5
+ task :scrub => :environment do
6
+ logger = ENV["VERBOSE"] ? Logger.new(STDOUT) : Rails.logger
7
+ logger = Logger.new(STDOUT)
8
+ logger.level = ENV["VERBOSE"] ? Logger::DEBUG : Logger::WARN
9
+ begin
10
+ Brillo.scrub!(logger: logger)
11
+ rescue CredentialsError => e
12
+ puts e
13
+ exit(1)
14
+ end
15
+ end
16
+
17
+ desc 'Load a previously created scrubbed database copy from S3'
18
+ task :load => :environment do
19
+ begin
20
+ Brillo.load!
21
+ rescue CredentialsError => e
22
+ puts e
23
+ exit(1)
24
+ end
25
+ end
26
+ end
metadata ADDED
@@ -0,0 +1,191 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: brillo
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Matt Bessey
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2016-05-29 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rake
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '10.0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '10.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: capistrano
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '3.0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '3.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: polo
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '0.3'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '0.3'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rails
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '3.2'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '3.2'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rspec
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '3.4'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '3.4'
83
+ - !ruby/object:Gem::Dependency
84
+ name: pry
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: benchmark-ips
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ - !ruby/object:Gem::Dependency
112
+ name: geminabox
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - ">="
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - ">="
123
+ - !ruby/object:Gem::Version
124
+ version: '0'
125
+ description:
126
+ email:
127
+ - mbessey@caring.com
128
+ executables: []
129
+ extensions: []
130
+ extra_rdoc_files: []
131
+ files:
132
+ - ".gitignore"
133
+ - ".rspec"
134
+ - ".travis.yml"
135
+ - CHANGELOG.md
136
+ - Gemfile
137
+ - LICENSE
138
+ - README.md
139
+ - Rakefile
140
+ - bin/console
141
+ - bin/setup
142
+ - brillo.gemspec
143
+ - config/brillo-example.yml
144
+ - config/brillo-initializer.rb
145
+ - lib/brillo.rb
146
+ - lib/brillo/adapter/README.md
147
+ - lib/brillo/adapter/base.rb
148
+ - lib/brillo/adapter/mysql.rb
149
+ - lib/brillo/adapter/postgres.rb
150
+ - lib/brillo/config.rb
151
+ - lib/brillo/dumper/mysql_dumper.rb
152
+ - lib/brillo/errors.rb
153
+ - lib/brillo/helpers/exec_helper.rb
154
+ - lib/brillo/loader.rb
155
+ - lib/brillo/logger.rb
156
+ - lib/brillo/railtie.rb
157
+ - lib/brillo/scrubber.rb
158
+ - lib/brillo/transferrer/s3.rb
159
+ - lib/brillo/validator.rb
160
+ - lib/brillo/version.rb
161
+ - lib/capistrano/brillo.rb
162
+ - lib/capistrano/tasks/db.cap
163
+ - lib/generators/brillo.rb
164
+ - lib/tasks/brillo.rake
165
+ homepage: https://github.com/bessey/brillo
166
+ licenses:
167
+ - MIT
168
+ metadata: {}
169
+ post_install_message:
170
+ rdoc_options: []
171
+ require_paths:
172
+ - lib
173
+ required_ruby_version: !ruby/object:Gem::Requirement
174
+ requirements:
175
+ - - ">="
176
+ - !ruby/object:Gem::Version
177
+ version: '0'
178
+ required_rubygems_version: !ruby/object:Gem::Requirement
179
+ requirements:
180
+ - - ">="
181
+ - !ruby/object:Gem::Version
182
+ version: '0'
183
+ requirements: []
184
+ rubyforge_project:
185
+ rubygems_version: 2.2.2
186
+ signing_key:
187
+ specification_version: 4
188
+ summary: Rails database scrubber and loader, great for seeding your dev db with real
189
+ but sanitized data
190
+ test_files: []
191
+ has_rdoc: