brillo 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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: