syncoku 0.0.1

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: f4fb8a94d545cfbd3ceca353015892221e9cb754
4
+ data.tar.gz: b2d8fd4509402e592ae9f70ce314402018b530c2
5
+ SHA512:
6
+ metadata.gz: 0e6aa7c4c8d0c6152069c95f517a954f7d5af33a7cf674220ab575998c82c6d285fd4e99b854bfdf54ed0e8d0db73cdca01082a35c5ec523e0b94abc59196896
7
+ data.tar.gz: e02b9ab5796c8b1abd3fcb8c03dc754b4d9b70677291cbf9c3854a2cdc56e69b4c00148142a3ecaf583589dc3902b2f5227e191195dc773db282b36f612511cc
data/.gitignore ADDED
@@ -0,0 +1,16 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+ *.bundle
11
+ *.so
12
+ *.o
13
+ *.a
14
+ mkmf.log
15
+
16
+ .ruby-version
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in syncoku.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2014 Bill Horsman
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,80 @@
1
+ # Syncoku
2
+
3
+ Copies a production Heroku Postgresql database to the local development database or a staging Heroku database. Optionally syncs the production S3 bucket with another bucket.
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ ```ruby
10
+ gem 'syncoku'
11
+ ```
12
+
13
+ And then execute:
14
+
15
+ $ bundle
16
+
17
+ Or install it yourself as:
18
+
19
+ $ gem install syncoku
20
+
21
+ ## Usage
22
+
23
+ To copy production to your local development database:
24
+ ```
25
+ syncoku
26
+ ```
27
+
28
+ If you have S3 configured it will do the database and S3. To choose just one of those:
29
+ ```
30
+ syncoku db
31
+ syncoku s3
32
+ ```
33
+
34
+ To target the staging environment:
35
+ ```
36
+ syncoku staging
37
+ ```
38
+
39
+ or more simply:
40
+ ```
41
+ syncoku s
42
+ ```
43
+
44
+ ## Downloading the database (locally)
45
+
46
+ It will capture a backup of the database and download it to a local file called `.syncoku.dump`. If you run Syncoku a second time and it discovers this file then it will give you the option of reusing it or downloading a new one. Reusing the existing one comes in useful if you have messed around with the local database and want to clean it up.
47
+
48
+ ## Hooks
49
+
50
+ If you define a rake task called `syncoku:after_sync` then it will automatically be run after the database has been restored and migrated. This is a good place to put anonymization tasks, for instance.
51
+
52
+ ## S3
53
+
54
+ If you add a file called `syncoku.yml` with the following information, it can sync between S3 buckets too:
55
+
56
+ ```
57
+ # syncoku.yml
58
+ s3:
59
+ access_key_id: "ABCDEFGH123456789"
60
+ secret_access_key: "a1secret2key3to4access5s3"
61
+
62
+ development:
63
+ bucket: "my-bucket-development"
64
+
65
+ staging:
66
+ bucket: "my-bucket-staging"
67
+
68
+ production:
69
+ bucket: "my-bucket-production"
70
+ ```
71
+
72
+ *Note:* a limitation is that that the buckets must use the same credentials.
73
+
74
+ ## Contributing
75
+
76
+ 1. Fork it ( https://github.com/billhorsman/syncoku/fork )
77
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
78
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
79
+ 4. Push to the branch (`git push origin my-new-feature`)
80
+ 5. Create a new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,2 @@
1
+ require "bundler/gem_tasks"
2
+
data/bin/syncoku ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'rubygems'
4
+ $LOAD_PATH.unshift(File.dirname(__FILE__) + '/../lib') unless $LOAD_PATH.include?(File.dirname(__FILE__) + '/../lib')
5
+ require 'bundler'
6
+ require 'syncoku'
7
+
8
+ Syncoku::Control.run ARGV.dup
@@ -0,0 +1,21 @@
1
+ module Syncoku
2
+ module CaptureBackup
3
+ include Runnable
4
+
5
+ def capture
6
+ puts "Capturing #{production_app_name} backup..."
7
+ run_on_production("pgbackups:capture --expire")
8
+ run_on_production("pgbackups:url")
9
+ end
10
+
11
+ def run_on_production(command)
12
+ run_command "heroku #{command} --app #{production_app_name}"
13
+ end
14
+
15
+
16
+ def production_app_name
17
+ @production_app_name ||= run_command("git remote -v | grep production | grep push").match(/heroku[^:]*:(.*)\.git/)[1]
18
+ end
19
+
20
+ end
21
+ end
@@ -0,0 +1,52 @@
1
+ module Syncoku
2
+
3
+ module Control
4
+ extend Syncoku::Runnable
5
+
6
+ def self.run(args)
7
+ matching_remotes = remotes & args
8
+ if matching_remotes.size == 0 && remote_index_uniq?
9
+ if key = (remote_index.keys & args)[0]
10
+ matching_remotes = [remote_index[key]]
11
+ args.delete key
12
+ end
13
+ end
14
+ target = case matching_remotes.compact.size
15
+ when 0
16
+ Syncoku::Local.new
17
+ when 1
18
+ remote = matching_remotes[0]
19
+ args.delete remote
20
+ Syncoku::Remote.new(remote)
21
+ else
22
+ puts "Please choose just one remote out of #{remotes.join(" or ")}"
23
+ exit 1
24
+ end
25
+ commands = %w[both db s3 rebuild] & args
26
+ commands << "both" if commands.size == 0
27
+ if commands.size > 1
28
+ puts "Choose just one command"
29
+ exit 1
30
+ else
31
+ args.delete commands[0]
32
+ target.send(commands[0], args)
33
+ end
34
+ end
35
+
36
+ def self.remotes
37
+ @remotes ||= run_command("git remote -v | grep heroku | grep push").split("\n").map {|line|
38
+ line.match(/^(.*)\t/)[1]
39
+ }.reject {|r| r == "production" || r == "heroku" }
40
+ end
41
+
42
+ def self.remote_index_uniq?
43
+ remote_index.size == remotes.size
44
+ end
45
+
46
+ def self.remote_index
47
+ @remote_index ||= Hash[remotes.map{|r| [r.slice(0, 1), r] }]
48
+ end
49
+
50
+ end
51
+
52
+ end
@@ -0,0 +1,26 @@
1
+ module Syncoku
2
+
3
+ # Responsible for syncing to a local app
4
+ class Local
5
+ include Runnable
6
+ include CaptureBackup
7
+
8
+ def both(args)
9
+ db(args)
10
+ s3(args) if S3.config?
11
+ end
12
+
13
+ def db(args)
14
+ Syncoku::LocalDb.new.sync
15
+ end
16
+
17
+ def s3(args)
18
+ Syncoku::S3.new(:development).sync
19
+ end
20
+
21
+ def rebuild(args)
22
+ Syncoku::LocalDb.new.rebuild
23
+ end
24
+
25
+ end
26
+ end
@@ -0,0 +1,112 @@
1
+ module Syncoku
2
+
3
+ class LocalDb
4
+ include Runnable
5
+ include CaptureBackup
6
+
7
+ def sync
8
+ if File.exist?("#{dump_filename}")
9
+ ask_to_download
10
+ else
11
+ download
12
+ end
13
+ drop_and_create
14
+ pg_restore
15
+ migrate
16
+ run_hook 'after_sync'
17
+ `touch tmp/restart.txt`
18
+ end
19
+
20
+ def rebuild
21
+ kill_connections
22
+ puts "Rebuilding database"
23
+ run_command "bundle exec rake db:drop db:create db:migrate"
24
+ puts "Seeding"
25
+ run_command "bundle exec rake db:seed"
26
+ end
27
+
28
+ private
29
+
30
+ def run_hook(name)
31
+ if test_command "rake syncoku:#{name}"
32
+ puts "Running #{name} hook"
33
+ run_command "bundle exec rake syncoku:#{name}"
34
+ else
35
+ puts "Skipping #{name} hook. Define a Rake task called syncoku:#{name} to activate."
36
+ end
37
+ end
38
+
39
+ def ask_to_download
40
+ print "Found existing #{dump_filename}. Choose:\n D = download new backup, or\n R = reuse existing backup\nPress D or R or anything else to abort: "
41
+ proceed = STDIN.getch.downcase
42
+ puts proceed
43
+ if proceed == 'd'
44
+ download
45
+ elsif proceed == 'r'
46
+ puts "OK, reusing backup"
47
+ else
48
+ exit 1
49
+ end
50
+ end
51
+
52
+ def download
53
+ run_command "curl -o #{dump_filename} \"#{capture}\""
54
+ end
55
+
56
+ def kill_connections
57
+ pids = `ps x|grep postgres|grep #{database_config["database"]} | grep -v 'grep' | cut -b 1,2,3,4,5,6`.gsub(/[^0-9]/, ' ').split(' ')
58
+ if pids.any?
59
+ puts "Killing #{pids.size} Postgres connection(s) (#{pids.join(", ")})"
60
+ pids.each do |pid|
61
+ `kill #{pid}`
62
+ end
63
+ else
64
+ puts "No connections to kill"
65
+ end
66
+ end
67
+
68
+ def drop_and_create
69
+ kill_connections
70
+ puts "Dropping and recreating #{database_name} database"
71
+ run_command "bundle exec rake db:drop db:create"
72
+ end
73
+
74
+ def migrate
75
+ run_command "bundle exec rake db:migrate"
76
+ end
77
+
78
+ def pg_restore
79
+ puts "Restoring database from #{dump_filename}"
80
+ options = []
81
+ options << "--verbose"
82
+ options << "--clean"
83
+ options << "--no-acl"
84
+ options << "--no-owner"
85
+ options << "--username=#{database_config["user"]}" if database_config["user"]
86
+ options << "--password=#{database_config["password"]}" if database_config["password"]
87
+ options << "--dbname=#{database_name}"
88
+ options << "--port=#{database_config["port"] || "5432"}"
89
+ output = `pg_restore #{options.join(' ')} #{dump_filename} 2> /dev/null`
90
+ if output =~ /a transfer is currently in progress/
91
+ puts "It looks like a backup is already in progress (or possibly stuck):"
92
+ puts output
93
+ `heroku pgbackups --app #{production_app}`
94
+ puts "Use pgbackups:destroy to remove the offending backup (or wait a bit to see if it fixes itself)"
95
+ exit 1
96
+ end
97
+ end
98
+
99
+ def database_name
100
+ database_config["database"]
101
+ end
102
+
103
+ def database_config
104
+ YAML.load(File.read("config/database.yml"))["development"]
105
+ end
106
+
107
+ def dump_filename
108
+ ".syncoku.dump"
109
+ end
110
+
111
+ end
112
+ end
@@ -0,0 +1,35 @@
1
+ module Syncoku
2
+
3
+ # Responsible for syncing to a remote app
4
+ class Remote
5
+ include Runnable
6
+
7
+ attr_reader :remote
8
+
9
+ def initialize(remote)
10
+ @remote = remote
11
+ end
12
+
13
+ def both(args)
14
+ db(args)
15
+ s3(args) if S3.config?
16
+ end
17
+
18
+ def db(args)
19
+ Syncoku::RemoteDb.new(app_name).sync
20
+ end
21
+
22
+ def s3(args)
23
+ Syncoku::S3.new(remote).sync
24
+ end
25
+
26
+ def rebuild(args)
27
+ puts "Rebuild not implemented"
28
+ end
29
+
30
+ def app_name
31
+ @app_name ||= run_command("git remote -v | grep #{remote} | grep push").match(/heroku\.com:(.*)\.git/)[1]
32
+ end
33
+
34
+ end
35
+ end
@@ -0,0 +1,31 @@
1
+ module Syncoku
2
+
3
+ class RemoteDb
4
+ include Runnable
5
+ include CaptureBackup
6
+
7
+ attr_reader :app_name
8
+
9
+ def initialize(app_name)
10
+ @app_name = app_name
11
+ end
12
+
13
+ def sync
14
+ puts "Switch on maintenance mode"
15
+ run_remotely "maintenance:on"
16
+ puts "Restoring database"
17
+ run_remotely "pg:reset DATABASE_URL --confirm #{app_name}"
18
+ run_remotely "pgbackups:restore DATABASE_URL '#{capture}' --confirm #{app_name}"
19
+ run_remotely "run rake db:migrate"
20
+ run_remotely "run rake syncoku:after_sync"
21
+ run_remotely "restart"
22
+ puts "Switch off maintenance mode"
23
+ run_remotely "maintenance:off"
24
+ end
25
+
26
+ def run_remotely(command)
27
+ run_command "heroku #{command} --app #{app_name}"
28
+ end
29
+
30
+ end
31
+ end
@@ -0,0 +1,26 @@
1
+ module Syncoku
2
+ module Runnable
3
+
4
+ def run_command(command)
5
+ Bundler.with_clean_env {
6
+ out = `#{command}`
7
+ if $?.success?
8
+ out
9
+ else
10
+ puts "Error running command:"
11
+ puts command
12
+ puts out
13
+ exit $?.exitstatus
14
+ end
15
+ }
16
+ end
17
+
18
+ def test_command(command)
19
+ Bundler.with_clean_env {
20
+ `#{command} 2> /dev/null`
21
+ $?.success?
22
+ }
23
+ end
24
+
25
+ end
26
+ end
data/lib/syncoku/s3.rb ADDED
@@ -0,0 +1,93 @@
1
+ module Syncoku
2
+
3
+ class S3
4
+
5
+ attr_reader :to_env, :from_name, :to_name, :from_bucket, :to_bucket, :from_keys, :to_keys
6
+
7
+ def initialize(to_env)
8
+ @to_env = to_env
9
+ end
10
+
11
+ def sync
12
+ @missing = []
13
+ @from_name = config_value "production.bucket"
14
+ @to_name = config_value "#{to_env}.bucket"
15
+ access_key_id = config_value "access_key_id"
16
+ secret_access_key = config_value "secret_access_key"
17
+ if @missing.any?
18
+ puts "Missing syncoku.yml values prevented S3 sync"
19
+ @missing.each do |path|
20
+ puts " s3.#{path}"
21
+ end
22
+ return
23
+ end
24
+ puts "Syncing S3 from #{from_name} to #{to_name}..."
25
+ AWS.config access_key_id: access_key_id, secret_access_key: secret_access_key
26
+ @from_bucket = AWS::S3.new.buckets[from_name]
27
+ @to_bucket = AWS::S3.new.buckets[to_name]
28
+ return unless get_keys
29
+ if !simple_sync & !remove_spare
30
+ puts "S3 is in sync. Nothing to do :)"
31
+ end
32
+ end
33
+
34
+ def get_keys
35
+ @from_keys = from_bucket.objects.map(&:key)
36
+ @to_keys = to_bucket.objects.map(&:key)
37
+ true
38
+ rescue AWS::S3::Errors::SignatureDoesNotMatch => e
39
+ puts "Can't sync S3 because #{e.message.sub(/^T/, 't')}"
40
+ false
41
+ end
42
+
43
+ def simple_sync
44
+ missing = from_keys - to_keys
45
+ return false if missing.empty?
46
+ puts "On #{from_name} but not on #{to_name}: #{"%7d" % missing.size}"
47
+ puts "Copying to #{to_name}"
48
+ missing.each do |key|
49
+ to_bucket.objects[key].copy_from key, { bucket: from_bucket, acl: :public_read}
50
+ print "."
51
+ STDOUT.flush
52
+ end
53
+ puts " done"
54
+ true
55
+ end
56
+
57
+ def remove_spare
58
+ spare = to_keys - from_keys
59
+ return false if spare.empty?
60
+ puts "On #{to_name} but not on #{from_name}: #{"%7d" % spare.size}"
61
+ puts "Deleting from #{to_name}"
62
+ spare.each do |key|
63
+ to_bucket.objects[key].delete
64
+ print "."
65
+ STDOUT.flush
66
+ end
67
+ puts " done"
68
+ true
69
+ end
70
+
71
+ def config_value(path)
72
+ value = config.dup["s3"]
73
+ path.split(".").each do |name|
74
+ value = value[name]
75
+ if value.nil?
76
+ @missing << path
77
+ return nil
78
+ end
79
+ end
80
+ value
81
+ end
82
+
83
+ def config
84
+ @config ||= YAML.load(File.read("syncoku.yml"))
85
+ end
86
+
87
+ def self.config?
88
+ File.exist?("syncoku.yml")
89
+ end
90
+
91
+ end
92
+
93
+ end
@@ -0,0 +1,3 @@
1
+ module Syncoku
2
+ VERSION = "0.0.1"
3
+ end
data/lib/syncoku.rb ADDED
@@ -0,0 +1,14 @@
1
+ require 'aws-sdk-v1'
2
+ require 'ostruct'
3
+ require 'rake'
4
+ require 'yaml'
5
+ require 'io/console'
6
+ require "syncoku/runnable"
7
+ require "syncoku/capture_backup"
8
+ require "syncoku/control"
9
+ require "syncoku/local"
10
+ require "syncoku/local_db"
11
+ require "syncoku/remote"
12
+ require "syncoku/remote_db"
13
+ require "syncoku/s3"
14
+ require "syncoku/version"
data/syncoku.gemspec ADDED
@@ -0,0 +1,23 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'syncoku/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "syncoku"
8
+ spec.version = Syncoku::VERSION
9
+ spec.authors = ["Bill Horsman"]
10
+ spec.email = ["bill@logicalcobwebs.com"]
11
+ spec.summary = %q{Convenient way of syncing data from and to Heroku}
12
+ spec.homepage = "https://github.com/billhorsman/syncoku"
13
+ spec.license = "MIT"
14
+
15
+ spec.files = `git ls-files -z`.split("\x0")
16
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
17
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
18
+ spec.require_paths = ["lib"]
19
+
20
+ spec.add_development_dependency "bundler", "~> 1.7"
21
+ spec.add_development_dependency "aws-sdk-v1", "~> 1.7"
22
+ spec.add_development_dependency "rake", "~> 10.0"
23
+ end
metadata ADDED
@@ -0,0 +1,104 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: syncoku
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Bill Horsman
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2015-04-27 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.7'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.7'
27
+ - !ruby/object:Gem::Dependency
28
+ name: aws-sdk-v1
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '1.7'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '1.7'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rake
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '10.0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '10.0'
55
+ description:
56
+ email:
57
+ - bill@logicalcobwebs.com
58
+ executables:
59
+ - syncoku
60
+ extensions: []
61
+ extra_rdoc_files: []
62
+ files:
63
+ - ".gitignore"
64
+ - Gemfile
65
+ - LICENSE.txt
66
+ - README.md
67
+ - Rakefile
68
+ - bin/syncoku
69
+ - lib/syncoku.rb
70
+ - lib/syncoku/capture_backup.rb
71
+ - lib/syncoku/control.rb
72
+ - lib/syncoku/local.rb
73
+ - lib/syncoku/local_db.rb
74
+ - lib/syncoku/remote.rb
75
+ - lib/syncoku/remote_db.rb
76
+ - lib/syncoku/runnable.rb
77
+ - lib/syncoku/s3.rb
78
+ - lib/syncoku/version.rb
79
+ - syncoku.gemspec
80
+ homepage: https://github.com/billhorsman/syncoku
81
+ licenses:
82
+ - MIT
83
+ metadata: {}
84
+ post_install_message:
85
+ rdoc_options: []
86
+ require_paths:
87
+ - lib
88
+ required_ruby_version: !ruby/object:Gem::Requirement
89
+ requirements:
90
+ - - ">="
91
+ - !ruby/object:Gem::Version
92
+ version: '0'
93
+ required_rubygems_version: !ruby/object:Gem::Requirement
94
+ requirements:
95
+ - - ">="
96
+ - !ruby/object:Gem::Version
97
+ version: '0'
98
+ requirements: []
99
+ rubyforge_project:
100
+ rubygems_version: 2.4.5
101
+ signing_key:
102
+ specification_version: 4
103
+ summary: Convenient way of syncing data from and to Heroku
104
+ test_files: []