heroku_hatchet 0.1.1 → 0.2.0

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