heroku_hatchet 0.1.1 → 0.2.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: bf055d51d17efef64a57f9d7cc5ba0efdfe29b3a
4
- data.tar.gz: b51a1720cf9e6eebf7d0280ebca26efe68f431ac
3
+ metadata.gz: 7f6fe23a7e5a091233694be4bc546232d45b13fe
4
+ data.tar.gz: 4c161fbcb5e7b5140b6025caa6a1b511635617c1
5
5
  SHA512:
6
- metadata.gz: 57a539206481a90f0118242f5af0587fd448abaf45dbcedf2fdf3be6e998d78d22feea5f80e7d7a5f4b652f167ea5467cfffdbbe6020e9b93686c1b1fa3c9000
7
- data.tar.gz: 0af1e3c1a8c9a957d6dab4b987da269fef9a69c44adcebc647f4e8ee4965c184472c506ed0bb9e1516dc5ac33692d5335202d35f14bcef2d547ea361d3d19c91
6
+ metadata.gz: 22409adecd80f6761593dd2f4cfb68696722903294cc08e7b5fb45183661d12e7b16ed1f67dd51a726c57e6249a9105a331ab9f0e1e3541a90c381384a1454ec
7
+ data.tar.gz: 1c95c1a1777355e891760054b1b75ec3cf4c70963271bd492cfc1beb9496e6e811e3e288650041a0ec0583a5c358266ac17b3060cce4f292efa0f0fb7580855b
data/.gitignore CHANGED
@@ -4,3 +4,4 @@ test/fixtures/repos/*
4
4
 
5
5
 
6
6
  Gemfile.lock
7
+ debug.rb
data/.travis.yml CHANGED
@@ -3,8 +3,8 @@ language: ruby
3
3
  rvm:
4
4
  - 2.0.0
5
5
  before_script: bundle exec rake hatchet:setup_travis
6
- script: bundle exec parallel_test test/hatchet -n 6
7
-
6
+ script: bundle exec parallel_test test/hatchet -n 9
7
+ after_script: bundle exec rake hatchet:teardown_travis
8
8
 
9
9
  env:
10
10
  global:
data/CHANGELOG.md CHANGED
@@ -1,3 +1,17 @@
1
+ ## HEAD
2
+
3
+
4
+ ## 0.2.0
5
+
6
+ - Add database method App#add_database
7
+
8
+ - Drastically improved reliability of `app.run` outputs.
9
+
10
+ - Add `rake hatchet:teardown_travis` task to put in `travis.yml`:
11
+
12
+ after_script: bundle exec rake hatchet:teardown_travis
13
+
14
+
1
15
  ## 0.1.1
2
16
 
3
17
  - Allow auto retries of pushes by setting environment variable `HATCHET_RETRIES=3`
data/lib/hatchet/app.rb CHANGED
@@ -1,5 +1,4 @@
1
1
  module Hatchet
2
- RETRIES = Integer(ENV['HATCHET_RETRIES'] || 1)
3
2
  class App
4
3
  attr_reader :name, :directory
5
4
 
@@ -28,6 +27,15 @@ module Hatchet
28
27
  self.class.config
29
28
  end
30
29
 
30
+
31
+ def add_database(db_name = 'heroku-postgresql:dev', match_val = "HEROKU_POSTGRESQL_[A-Z]+_URL")
32
+ Hatchet::RETRIES.times.retry do
33
+ heroku.post_addon(name, db_name)
34
+ _, value = heroku.get_config_vars(name).body.detect {|k, v| k.match(/#{match_val}/) }
35
+ heroku.put_config_vars(name, 'DATABASE_URL' => value)
36
+ end
37
+ end
38
+
31
39
  # runs a command on heroku similar to `$ heroku run #foo`
32
40
  # but programatically and with more control
33
41
  def run(command, timeout = nil, &block)
@@ -0,0 +1,39 @@
1
+ # removes the commands from strings retrieved from stuff like `heroku run bash`
2
+ # since likely you care about the output, not the input
3
+ # this is especially useful for seeing if a given input command has finished running
4
+ # if we cannot find a valid input command and output command return the full unparsed string
5
+ module Hatchet
6
+ class CommandParser
7
+ attr_accessor :command
8
+
9
+ def initialize(command)
10
+ @command = command
11
+ @parsed_string = ""
12
+ @raw_string = ""
13
+ end
14
+
15
+ def regex
16
+ /#{Regexp.quote(command)}\r*\n+/
17
+ end
18
+
19
+ def parse(string)
20
+ @raw_string = string
21
+ @parsed_string = string.split(regex).last
22
+ return self
23
+ end
24
+
25
+ def to_s
26
+ @parsed_string || @raw_string
27
+ end
28
+
29
+ def missing_valid_output?
30
+ !has_valid_output?
31
+ end
32
+
33
+ def has_valid_output?
34
+ return false unless @raw_string.match(regex)
35
+ return false if @parsed_string.blank? || @parsed_string.strip.blank?
36
+ true
37
+ end
38
+ end
39
+ end
@@ -3,18 +3,20 @@ module Hatchet
3
3
  # spawns a process on Heroku, and keeps it open for writing
4
4
  # like `heroku run bash`
5
5
  class ProcessSpawn
6
- attr_reader :command, :app, :timeout
7
-
6
+ attr_reader :command, :app, :timeout, :pid
8
7
  TIMEOUT = 60 # seconds to bring up a heroku command like `heroku run bash`
9
8
 
10
9
  def initialize(command, app, timeout = nil)
11
- @command = command
12
- @app = app
13
- @timeout = timeout || TIMEOUT
10
+ raise "need command" unless command.present?
11
+ raise "need app" unless app.present?
12
+ @command = "heroku run #{command} -a #{app.name}"
13
+ @ready_regex = "^run.*up.*#{command}"
14
+ @app = app
15
+ @timeout = timeout || TIMEOUT
14
16
  end
15
17
 
16
18
  def ready?
17
- @ready ||= `heroku ps -a #{app.name}`.match(/^run.*up.*#{command}/).present?
19
+ @ready ||= `heroku ps -a #{app.name}`.match(/#{@ready_regex}/).present?
18
20
  end
19
21
 
20
22
  def not_ready?
@@ -28,25 +30,38 @@ module Hatchet
28
30
  return true
29
31
  end
30
32
 
33
+ # some REPL's don't sync standard out by default
34
+ # try to do it auto-magically
35
+ def repl_magic(repl)
36
+ case command
37
+ when /rails\s*console/, /\sirb\s/
38
+ # puts "magic for: '#{command}'"
39
+ repl.run("STDOUT.sync = true")
40
+ end
41
+ end
42
+
31
43
  # Open up PTY (pseudo terminal) to command like `heroku run bash`
32
44
  # Wait for the dyno to deploy, then allow user to run arbitrary commands
33
- #
34
- def run(&block)
35
- raise "need app" unless app.present?
36
- raise "need command" unless command.present?
37
- heroku_command = "heroku run #{command} -a #{app.name}"
38
- return `#{heroku_command}` if block.blank? # one off command, no block given
39
-
40
- output, input, pid = PTY.spawn(heroku_command)
41
- stream = StreamExec.new(input, output)
45
+ def spawn_repl
46
+ output, input, pid = PTY.spawn(command)
47
+ stream = StreamExec.new(output, input, pid)
48
+ repl = ReplRunner.new(stream)
42
49
  stream.timeout("waiting for spawn", timeout) do
43
50
  wait_for_spawn!
44
51
  end
45
52
  raise "Could not run: '#{command}', command took longer than #{timeout} seconds" unless self.ready?
46
- yield stream
53
+
54
+ repl_magic(repl)
55
+ repl.wait_for_boot(5) # important to get rid of startup info i.e. "booting rails console ..."
56
+ return repl
57
+ end
58
+
59
+ def run(&block)
60
+ return `#{command}` if block.blank? # one off command, no block given
61
+
62
+ yield repl = spawn_repl
47
63
  ensure
48
- stream.close if stream.present?
49
- Process.kill('TERM', pid) if pid.present?
64
+ repl.close if repl.present?
50
65
  end
51
66
  end
52
67
  end
@@ -0,0 +1,61 @@
1
+ # takes a StringExec class and attempts to parse commands out of it
2
+ module Hatchet
3
+ class ReplRunner
4
+ TIMEOUT = 1
5
+ RETRIES = 10
6
+
7
+ attr_accessor :repl
8
+
9
+ def initialize(repl, command_parser_klass = CommandParser)
10
+ @repl = repl
11
+ @command_parser_klass = command_parser_klass
12
+ end
13
+
14
+ def command_parser_klass
15
+ @command_parser_klass
16
+ end
17
+
18
+ # adds a newline cause thats what most repl-s need to run command
19
+ def write(cmd)
20
+ repl.write("#{cmd}\n")
21
+ end
22
+
23
+ def run(cmd, options = {})
24
+ timeout = options[:timeout] || TIMEOUT
25
+ retries = options[:retries] || RETRIES
26
+
27
+ write(cmd)
28
+ read(cmd, timeout, retries)
29
+ end
30
+
31
+ def wait_for_boot(timeout = 5)
32
+ repl.read(timeout)
33
+ end
34
+
35
+ def close
36
+ repl.close
37
+ end
38
+
39
+ # take in a command like "ls", and tries to find it in the output
40
+ # of the repl (StreamExec)
41
+ # Example
42
+ # output, input, pid = PTY.spawn('sh')
43
+ # stream = StreamExec.new(output, input, pid)
44
+ # repl_runner = ReplRunner.new(stream)
45
+ # repl_runner.write("ls\n")
46
+ # repl_runner.read
47
+ # # => "app\tconfig.ru Gemfile\t LICENSE.txt public\t script vendor\r\r\nbin\tdb\t Gemfile.lock log\t Rakefile\t test\r\r\nconfig\tdoc\t lib\t\t Procfile README.md tmp\r\r\n"
48
+ #
49
+ # if the command "ls" is not found, repl runner will continue to retry grabbing more output
50
+ def read(cmd, timeout = TIMEOUT, retries = RETRIES)
51
+ str = ""
52
+ command_parser = command_parser_klass.new(cmd)
53
+ retries.times.each do
54
+ next if command_parser.has_valid_output?
55
+ str << repl.read(timeout)
56
+ command_parser.parse(str)
57
+ end
58
+ return command_parser.to_s
59
+ end
60
+ end
61
+ end
@@ -1,19 +1,26 @@
1
1
  require 'timeout'
2
+
2
3
  module Hatchet
3
4
  # runs arbitrary commands within a Heroku process
4
5
  class StreamExec
5
- attr_reader :input, :output
6
+ attr_reader :input, :output, :pid
6
7
  TIMEOUT = 1 # seconds to run an arbitrary command on a heroku process like `$ls`
7
8
 
8
- def initialize(input, output)
9
+ def initialize(output, input, pid)
9
10
  @input = input
10
11
  @output = output
12
+ @pid = pid
13
+ end
14
+
15
+ def write(cmd)
16
+ input.write(cmd)
17
+ rescue Errno::EIO => e
18
+ raise e, "#{e.message} | trying to write '#{cmd}'"
11
19
  end
12
20
 
13
- def run(cmd)
14
- raise "command expected" if cmd.blank?
15
- input.write("#{cmd}\n")
16
- return read(cmd)
21
+ def run(cmd, timeout = TIMEOUT)
22
+ write(cmd)
23
+ return read(timeout)
17
24
  end
18
25
 
19
26
  def close
@@ -21,6 +28,8 @@ module Hatchet
21
28
  input.close
22
29
  output.close
23
30
  end
31
+ ensure
32
+ Process.kill('TERM', pid) if pid.present?
24
33
  end
25
34
 
26
35
  # There be dragons - (You're playing with process deadlock)
@@ -29,21 +38,17 @@ module Hatchet
29
38
  # First pull all contents from stdout (except we don't know how many there are)
30
39
  # So we have to go until our process deadlocks, then we timeout and return the string
31
40
  #
32
- # Example
33
- # result = ""
34
- # input.write("ls\n")
35
- # Timeout::timeout(1) {output.each {|x| result << x}}
36
- # Timeout::Error: execution expired
37
- # puts result
38
- # # => "ls\r\r\napp\tconfig.ru Gemfile\t LICENSE.txt public\t script vendor\r\r\nbin\tdb\t Gemfile.lock log\t Rakefile\t test\r\r\nconfig\tdoc\t lib\t\t Procfile README.md tmp\r\r\n"
39
- #
40
- # Now we want to remove the original command ("ls\r\r\n") and return the remainder
41
- def read(cmd, str = "")
42
- timeout do
43
- # this is guaranteed to timeout; output.each will not return
44
- output.each { |line| str << line }
41
+ def read(timeout = TIMEOUT)
42
+ str = ""
43
+ while true
44
+ Timeout::timeout(timeout) do
45
+ str << output.readline
46
+ end
45
47
  end
46
- str.split("#{cmd}\r\r\n").last
48
+
49
+ return str
50
+ rescue Timeout::Error, EOFError
51
+ return str
47
52
  end
48
53
 
49
54
  def timeout(msg = nil, val = TIMEOUT, &block)
@@ -55,4 +60,3 @@ module Hatchet
55
60
  end
56
61
  end
57
62
  end
58
-
data/lib/hatchet/tasks.rb CHANGED
@@ -17,4 +17,11 @@ namespace :hatchet do
17
17
  end
18
18
  puts "== Done =="
19
19
  end
20
+
21
+ task :teardown_travis do
22
+ ['heroku keys:remove ~/.ssh/id_rsa'].each do |command|
23
+ puts "== Running: #{command}"
24
+ `#{command}`
25
+ end
26
+ end
20
27
  end
@@ -1,3 +1,3 @@
1
1
  module Hatchet
2
- VERSION = "0.1.1"
2
+ VERSION = "0.2.0"
3
3
  end
data/lib/hatchet.rb CHANGED
@@ -10,6 +10,8 @@ require 'stringio'
10
10
 
11
11
 
12
12
  module Hatchet
13
+ RETRIES = Integer(ENV['HATCHET_RETRIES'] || 1)
14
+
13
15
  class App
14
16
  end
15
17
  end
@@ -18,6 +20,8 @@ require 'hatchet/version'
18
20
  require 'hatchet/app'
19
21
  require 'hatchet/anvil_app'
20
22
  require 'hatchet/git_app'
23
+ require 'hatchet/command_parser'
21
24
  require 'hatchet/stream_exec'
25
+ require 'hatchet/repl_runner'
22
26
  require 'hatchet/process_spawn'
23
27
  require 'hatchet/config'
@@ -20,13 +20,13 @@ class AllowFailureAnvilTest < Test::Unit::TestCase
20
20
  end
21
21
 
22
22
  def test_retries
23
- Hatchet::App.const_set(:RETRIES, 2)
23
+ Hatchet.const_set(:RETRIES, 2)
24
24
  assert_raise(Anvil::Builder::BuildError) do
25
25
  app = Hatchet::AnvilApp.new("no_lockfile", buildpack: @buildpack_path)
26
26
  app.expects(:push!).twice.raises(Anvil::Builder::BuildError)
27
27
  app.deploy
28
28
  end
29
29
  ensure
30
- Hatchet::App.const_set(:RETRIES, 1)
30
+ Hatchet.const_set(:RETRIES, 1)
31
31
  end
32
32
  end
@@ -0,0 +1,54 @@
1
+ require 'test_helper'
2
+
3
+ class CommandParserTest < Test::Unit::TestCase
4
+ def test_removes_command_from_string
5
+ hash = {command: "1+1",
6
+ string: "1+1\r\r\n=> 2\r\r\n",
7
+ expect: "=> 2\r\r\n"
8
+ }
9
+ cp = Hatchet::CommandParser.new(hash[:command]).parse(hash[:string])
10
+ assert cp.has_valid_output?
11
+ assert_equal hash[:expect], cp.to_s
12
+
13
+
14
+ hash = {command: "ls",
15
+ string: "Running `bash` attached to terminal... up, run.8041\r\n\e[01;34m~\e[00m \e[01;32m$ \e[00mls\r\r\napp config\tdb Gemfile\t lib\tProcfile Rakefile script tmp\r\r\nbin config.ru\tdoc Gemfile.lock log\tpublic\t README.rdoc test vendor\r\r\n",
16
+ expect: "app config\tdb Gemfile\t lib\tProcfile Rakefile script tmp\r\r\nbin config.ru\tdoc Gemfile.lock log\tpublic\t README.rdoc test vendor\r\r\n"
17
+ }
18
+ cp = Hatchet::CommandParser.new(hash[:command]).parse(hash[:string])
19
+ assert cp.has_valid_output?
20
+ assert_equal hash[:expect], cp.to_s
21
+ end
22
+
23
+ def test_returns_result_if_no_command_in_result
24
+ hash = {command: "ls",
25
+ string: "1+1\r\r\n=> 2\r\r\n",
26
+ expect: "1+1\r\r\n=> 2\r\r\n"
27
+ }
28
+ cp = Hatchet::CommandParser.new(hash[:command]).parse(hash[:string])
29
+ refute cp.has_valid_output?
30
+ assert_equal hash[:expect], cp.to_s
31
+ end
32
+
33
+ def test_empty_string
34
+ hash = {command: "ls",
35
+ string: "",
36
+ expect: ""
37
+ }
38
+ cp = Hatchet::CommandParser.new(hash[:command]).parse(hash[:string])
39
+ refute cp.has_valid_output?
40
+ assert_equal hash[:expect], cp.to_s
41
+ end
42
+
43
+
44
+ def test_partial_command_no_result
45
+ hash = {command: "1+1",
46
+ string: "1+1\r\r\n",
47
+ expect: "1+1\r\r\n"
48
+ }
49
+ cp = Hatchet::CommandParser.new(hash[:command]).parse(hash[:string])
50
+ assert_equal hash[:expect], cp.to_s
51
+ refute cp.has_valid_output?
52
+ end
53
+ end
54
+
@@ -20,7 +20,7 @@ class ConfigTest < Test::Unit::TestCase
20
20
  def test_config_repos
21
21
  expected_repos = { "rails3_mri_193" => "test/fixtures/repos/rails3/rails3_mri_193",
22
22
  "rails2blog" => "test/fixtures/repos/rails2/rails2blog",
23
- "no_lockfile" =>"test/fixtures/repos/bundler/no_lockfile"}
23
+ "no_lockfile" => "test/fixtures/repos/bundler/no_lockfile"}
24
24
  assert_equal expected_repos, @config.repos
25
25
  end
26
26
 
@@ -0,0 +1,28 @@
1
+ require 'test_helper'
2
+
3
+ class MultiCmdRunnerTest < Test::Unit::TestCase
4
+ def setup
5
+ @buildpack_path = File.expand_path 'test/fixtures/buildpacks/heroku-buildpack-ruby'
6
+ end
7
+
8
+ # slow but needed, there are ghosts in the machine
9
+ # by running common command multiple times we can find them
10
+ def test_multi_repl_commands
11
+ Hatchet::AnvilApp.new("rails3_mri_193", buildpack: @buildpack_path).deploy do |app|
12
+ app.add_database
13
+
14
+ rand(3..7).times do
15
+ app.run("bash") do |bash|
16
+ assert_match /Gemfile/, bash.run("ls")
17
+ end
18
+ end
19
+
20
+ rand(3..7).times do
21
+ app.run("rails console") do |console|
22
+ assert_match /foofoofoofoofoo/, console.run("'foo' * 5")
23
+ assert_match /hello world/, console.run("'hello ' + 'world'")
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,20 @@
1
+ require 'test_helper'
2
+ require 'stringio'
3
+
4
+ class ReplRunnerTest < Test::Unit::TestCase
5
+
6
+ def test_returns_full_output_if_command_not_found
7
+ command = "irb"
8
+ input = StringIO.new("bar")
9
+ bogus_output = StringIO.new("foo")
10
+ stream = Hatchet::StreamExec.new(bogus_output, input, 1)
11
+ repl = Hatchet::ReplRunner.new(stream)
12
+ repl.write("1+1")
13
+ assert_equal bogus_output.string, repl.read("1+1")
14
+
15
+ Hatchet::CommandParser.any_instance.expects(:parse).times(Hatchet::ReplRunner::RETRIES)
16
+ Hatchet::CommandParser.any_instance.stubs(:to_s)
17
+ repl.write("1+1")
18
+ repl.read("1+1")
19
+ end
20
+ end
@@ -0,0 +1,12 @@
1
+ require 'test_helper'
2
+
3
+ class StreamExecTest < Test::Unit::TestCase
4
+ def test_local_irb_stream
5
+ command = "irb"
6
+ output, input, pid = PTY.spawn(command)
7
+ stream = Hatchet::StreamExec.new(output, input, pid)
8
+ stream.run("STDOUT.sync = true\n")
9
+ assert_equal "1+1\r\n => 2 \r\n", stream.run("1+1\n")
10
+ end
11
+ end
12
+
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: heroku_hatchet
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Richard Schneeman
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2013-05-23 00:00:00.000000000 Z
11
+ date: 2013-06-22 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: heroku-api
@@ -157,9 +157,11 @@ files:
157
157
  - lib/hatchet.rb
158
158
  - lib/hatchet/anvil_app.rb
159
159
  - lib/hatchet/app.rb
160
+ - lib/hatchet/command_parser.rb
160
161
  - lib/hatchet/config.rb
161
162
  - lib/hatchet/git_app.rb
162
163
  - lib/hatchet/process_spawn.rb
164
+ - lib/hatchet/repl_runner.rb
163
165
  - lib/hatchet/stream_exec.rb
164
166
  - lib/hatchet/tasks.rb
165
167
  - lib/hatchet/version.rb
@@ -202,8 +204,12 @@ files:
202
204
  - test/hatchet/allow_failure_anvil_test.rb
203
205
  - test/hatchet/allow_failure_git_test.rb
204
206
  - test/hatchet/anvil_test.rb
207
+ - test/hatchet/command_parser_test.rb
205
208
  - test/hatchet/config_test.rb
206
209
  - test/hatchet/git_test.rb
210
+ - test/hatchet/multi_cmd_runner_test.rb
211
+ - test/hatchet/repl_runner_test.rb
212
+ - test/hatchet/stream_exec_test.rb
207
213
  - test/test_helper.rb
208
214
  homepage: https://github.com/heroku/hatchet
209
215
  licenses:
@@ -269,6 +275,10 @@ test_files:
269
275
  - test/hatchet/allow_failure_anvil_test.rb
270
276
  - test/hatchet/allow_failure_git_test.rb
271
277
  - test/hatchet/anvil_test.rb
278
+ - test/hatchet/command_parser_test.rb
272
279
  - test/hatchet/config_test.rb
273
280
  - test/hatchet/git_test.rb
281
+ - test/hatchet/multi_cmd_runner_test.rb
282
+ - test/hatchet/repl_runner_test.rb
283
+ - test/hatchet/stream_exec_test.rb
274
284
  - test/test_helper.rb