harrison 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.
data/lib/harrison.rb ADDED
@@ -0,0 +1,91 @@
1
+ require "trollop"
2
+ require "harrison/version"
3
+ require "harrison/ssh"
4
+ require "harrison/config"
5
+ require "harrison/base"
6
+ require "harrison/package"
7
+ require "harrison/deploy"
8
+
9
+ module Harrison
10
+
11
+ def self.invoke(args)
12
+ @@args = args.freeze
13
+
14
+ abort("No command given. Run with --help for valid commands and options.") if @@args.empty?
15
+
16
+ # Catch root level --help
17
+ Harrison::Base.new.parse(@@args.dup) and exit(0) if @@args[0] == '--help'
18
+
19
+ # Find Harrisonfile.
20
+ hf = find_harrisonfile
21
+ abort("ERROR: Could not find a Harrisonfile in this directory or any ancestor.") if hf.nil?
22
+
23
+ # Find the class to handle command.
24
+ @@runner = find_runner(@@args[0])
25
+ abort("ERROR: Unrecognized command \"#{@@args[0]}\".") unless @@runner
26
+
27
+ # Eval the Harrisonfile.
28
+ eval_script(hf)
29
+
30
+ # Invoke command and cleanup afterwards.
31
+ begin
32
+ @@runner.call.run
33
+ ensure
34
+ @@runner.call.close
35
+ end
36
+ end
37
+
38
+ def self.config(opts={})
39
+ @@config ||= Harrison::Config.new(opts)
40
+
41
+ if block_given?
42
+ yield @@config
43
+ else
44
+ @@config
45
+ end
46
+ end
47
+
48
+ def self.package(opts={})
49
+ @@packager ||= Harrison::Package.new(opts)
50
+
51
+ # Parse options if this is the target command.
52
+ @@packager.parse(@@args.dup) if @@runner && @@runner.call == @@packager
53
+
54
+ yield @@packager
55
+ end
56
+
57
+ def self.deploy(opts={})
58
+ @@deployer ||= Harrison::Deploy.new(opts)
59
+
60
+ # Parse options if this is the target command.
61
+ @@deployer.parse(@@args.dup) if @@runner && @@runner.call == @@deployer
62
+
63
+ yield @@deployer
64
+ end
65
+
66
+
67
+ private
68
+
69
+ def self.find_harrisonfile
70
+ previous = nil
71
+ current = File.expand_path(Dir.pwd)
72
+
73
+ until !File.directory?(current) || current == previous
74
+ filename = File.join(current, 'Harrisonfile')
75
+ return filename if File.file?(filename)
76
+ current, previous = File.expand_path("..", current), current
77
+ end
78
+ end
79
+
80
+ def self.eval_script(filename)
81
+ proc = Proc.new {}
82
+ eval(File.read(filename), proc.binding, filename)
83
+ end
84
+
85
+ def self.find_runner(command)
86
+ case command.downcase
87
+ when 'package' then lambda { @@packager if self.class_variable_defined?(:@@packager) }
88
+ when 'deploy' then lambda { @@deployer if self.class_variable_defined?(:@@deployer) }
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,34 @@
1
+ # This is an valid fixture Harrisonfile used for testing.
2
+
3
+ # Project-wide Config
4
+ Harrison.config do |h|
5
+ h.project = 'harrison'
6
+ h.git_src = "git@github.com:scotje/harrison.git"
7
+ end
8
+
9
+ Harrison.package do |h|
10
+ # Where to build package.
11
+ h.host = 'localhost'
12
+ h.user = 'test'
13
+
14
+ # Things we don't want to package.
15
+ h.exclude = %w(.git config coverage examples log module_files pkg tmp spec)
16
+
17
+ # Define the build process here.
18
+ h.run do |h|
19
+ h.remote_exec("echo \"test-package\"")
20
+ end
21
+ end
22
+
23
+ Harrison.deploy do |h|
24
+ # Hosts to deploy to.
25
+ h.hosts = [ 'localhost' ]
26
+
27
+ h.user = 'test'
28
+ h.base_dir = '/tmp'
29
+
30
+ # Run block will be invoked once for each host after new code is in place.
31
+ h.run do |h|
32
+ h.remote_exec("echo \"test-deploy\"")
33
+ end
34
+ end
@@ -0,0 +1 @@
1
+ puts "this file was eval-led"
File without changes
@@ -0,0 +1,83 @@
1
+ require 'bundler/setup'
2
+ Bundler.setup
3
+
4
+ require 'harrison'
5
+
6
+ # See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
7
+ RSpec.configure do |config|
8
+ config.treat_symbols_as_metadata_keys_with_true_values = true
9
+ config.run_all_when_everything_filtered = true
10
+ config.filter_run :focus
11
+
12
+ # Run specs in random order to surface order dependencies. If you find an
13
+ # order dependency and want to debug it, you can fix the order by providing
14
+ # the seed, which is printed after each run.
15
+ # --seed 1234
16
+ config.order = 'random'
17
+ end
18
+
19
+ RSpec::Matchers.define :exit_with_code do |exp_code|
20
+ actual = nil
21
+
22
+ match do |block|
23
+ begin
24
+ block.call
25
+ rescue SystemExit => e
26
+ actual = e.status
27
+ end
28
+ actual and actual == exp_code
29
+ end
30
+
31
+ failure_message_for_should do |block|
32
+ "expected block to call exit(#{exp_code}) but exit" +
33
+ (actual.nil? ? " not called" : "(#{actual}) was called")
34
+ end
35
+
36
+ failure_message_for_should_not do |block|
37
+ "expected block not to call exit(#{exp_code})"
38
+ end
39
+
40
+ description do
41
+ "expect block to call exit(#{exp_code})"
42
+ end
43
+ end
44
+
45
+ def capture(io_names = [ :stdout, :stderr ], &block)
46
+ original_ios = {}
47
+ fake_ios = {}
48
+
49
+ io_names = [ io_names ] unless io_names.respond_to?(:each)
50
+
51
+ io_names.each do |io_name|
52
+ original_ios[io_name] = eval("$#{io_name}")
53
+ fake_ios[io_name] = StringIO.new
54
+
55
+ eval("$#{io_name} = fake_ios[io_name]")
56
+ end
57
+
58
+ begin
59
+ yield
60
+ ensure
61
+ io_names.each do |io_name|
62
+ eval("$#{io_name} = original_ios[io_name]")
63
+ end
64
+ end
65
+
66
+ if io_names.size == 1
67
+ return fake_ios[io_names.first].string.downcase
68
+ else
69
+ return fake_ios.each { |io, output| fake_ios[io] = output.string.downcase }
70
+ end
71
+ end
72
+
73
+ def fixture_path
74
+ File.dirname(__FILE__) + "/fixtures"
75
+ end
76
+
77
+ def harrisonfile_fixture_path(type=nil)
78
+ if type
79
+ fixture_path + "/Harrisonfile.#{type}"
80
+ else
81
+ fixture_path + "/Harrisonfile"
82
+ end
83
+ end
@@ -0,0 +1,221 @@
1
+ require 'spec_helper'
2
+
3
+ describe Harrison::Base do
4
+ let(:instance) { Harrison::Base.new }
5
+
6
+ describe 'initialize' do
7
+ it 'should persist arg_opts' do
8
+ instance = Harrison::Base.new(['foo'])
9
+
10
+ instance.instance_variable_get('@arg_opts').should include('foo')
11
+ end
12
+
13
+ it 'should add debug to arg_opts' do
14
+ instance.instance_variable_get('@arg_opts').to_s.should include(':debug')
15
+ end
16
+
17
+ it 'should persist options' do
18
+ instance = Harrison::Base.new([], testopt: 'foo')
19
+
20
+ instance.instance_variable_get('@options').should include(testopt: 'foo')
21
+ end
22
+ end
23
+
24
+ describe 'class methods' do
25
+ describe '.option_helper' do
26
+ it 'should define a getter instance method for the option' do
27
+ Harrison::Base.option_helper('foo')
28
+
29
+ instance.methods.should include(:foo)
30
+ end
31
+
32
+ it 'should define a setter instance method for the option' do
33
+ Harrison::Base.option_helper('foo')
34
+
35
+ instance.methods.should include(:foo=)
36
+ end
37
+ end
38
+ end
39
+
40
+ describe 'instance methods' do
41
+ describe '#exec' do
42
+ it 'should execute a command locally and return the output' do
43
+ instance.exec('echo "foo"').should == 'foo'
44
+ end
45
+
46
+ it 'should complain if command returns non-zero' do
47
+ output = capture(:stderr) do
48
+ lambda { instance.exec('cat noexist 2>/dev/null') }.should exit_with_code(1)
49
+ end
50
+
51
+ output.should include('unable', 'execute', 'local', 'command')
52
+ end
53
+ end
54
+
55
+ describe '#remote_exec' do
56
+ before(:each) do
57
+ @mock_ssh = double(:ssh)
58
+ expect(instance).to receive(:ssh).and_return(@mock_ssh)
59
+ end
60
+
61
+ it 'should delegate command to ssh instance' do
62
+ expect(@mock_ssh).to receive(:exec).and_return('remote_exec_return')
63
+
64
+ instance.remote_exec('remote exec').should == 'remote_exec_return'
65
+ end
66
+
67
+ it 'should complain if command returns nil' do
68
+ expect(@mock_ssh).to receive(:exec).and_return(nil)
69
+
70
+ output = capture(:stderr) do
71
+ lambda { instance.remote_exec('remote exec fail') }.should exit_with_code(1)
72
+ end
73
+
74
+ output.should include('unable', 'execute', 'remote', 'command')
75
+ end
76
+ end
77
+
78
+ describe '#parse' do
79
+ it 'should recognize options from the command line' do
80
+ instance = Harrison::Base.new([
81
+ [ :testopt, "Test option.", :type => :string ]
82
+ ])
83
+
84
+ instance.parse(%w(test --testopt foozle))
85
+
86
+ instance.options.should include({testopt: 'foozle'})
87
+ end
88
+
89
+ it 'should set the debug flag on the module when passed --debug' do
90
+ instance.parse(%w(test --debug))
91
+
92
+ Harrison::DEBUG.should be_true
93
+ end
94
+ end
95
+
96
+ describe '#run' do
97
+ context 'when given a block' do
98
+ it 'should store the block' do
99
+ test_block = Proc.new { |test| "block_output" }
100
+ instance.run(&test_block)
101
+
102
+ instance.instance_variable_get("@run_block").should == test_block
103
+ end
104
+ end
105
+
106
+ context 'when not given a block' do
107
+ it 'should return nil if no block stored' do
108
+ instance.run.should == nil
109
+ end
110
+
111
+ it 'should invoke the previously stored block if it exists' do
112
+ test_block = Proc.new { |test| "block_output" }
113
+ instance.run(&test_block)
114
+
115
+ instance.run.should == "block_output"
116
+ end
117
+ end
118
+ end
119
+
120
+ describe '#download' do
121
+ before(:each) do
122
+ @mock_ssh = double(:ssh)
123
+ expect(instance).to receive(:ssh).and_return(@mock_ssh)
124
+ end
125
+
126
+ it 'should delegate downloads to the SSH class' do
127
+ expect(@mock_ssh).to receive(:download).with('remote', 'local').and_return(true)
128
+
129
+ instance.download('remote', 'local').should == true
130
+ end
131
+ end
132
+
133
+ describe '#upload' do
134
+ before(:each) do
135
+ @mock_ssh = double(:ssh)
136
+ expect(instance).to receive(:ssh).and_return(@mock_ssh)
137
+ end
138
+
139
+ it 'should delegate uploads to the SSH class' do
140
+ expect(@mock_ssh).to receive(:upload).with('local', 'remote').and_return(true)
141
+
142
+ instance.upload('local', 'remote').should == true
143
+ end
144
+ end
145
+
146
+ describe '#close' do
147
+ before(:each) do
148
+ @mock_ssh = double(:ssh)
149
+ instance.instance_variable_set('@ssh', @mock_ssh)
150
+ expect(instance).to receive(:ssh).and_return(@mock_ssh)
151
+ end
152
+
153
+ it 'should invoke close on ssh instance' do
154
+ expect(@mock_ssh).to receive(:close).and_return(true)
155
+
156
+ instance.close.should == true
157
+ end
158
+ end
159
+ end
160
+
161
+ describe 'protected methods' do
162
+ describe '#ssh' do
163
+ it 'should instantiate a new ssh instance if needed' do
164
+ mock_ssh = double(:ssh)
165
+ expect(Harrison::SSH).to receive(:new).and_return(mock_ssh)
166
+
167
+ instance.send(:ssh).should == mock_ssh
168
+ end
169
+
170
+ it 'should return previously instantiated ssh instance' do
171
+ mock_ssh = double(:ssh)
172
+ instance.instance_variable_set('@ssh', mock_ssh)
173
+ expect(Harrison::SSH).to_not receive(:new)
174
+
175
+ instance.send(:ssh).should == mock_ssh
176
+ end
177
+ end
178
+
179
+ describe '#ensure_local_dir' do
180
+ it 'should try to create a directory locally' do
181
+ expect(instance).to receive(:system).with(/local_dir/).and_return(true)
182
+
183
+ instance.send(:ensure_local_dir, 'local_dir').should == true
184
+ end
185
+
186
+ it 'should only try to create a directory once' do
187
+ expect(instance).to receive(:system).with(/local_dir/).once.and_return(true)
188
+
189
+ instance.send(:ensure_local_dir, 'local_dir').should == true
190
+ instance.send(:ensure_local_dir, 'local_dir').should == true
191
+ end
192
+ end
193
+
194
+ describe '#ensure_remote_dir' do
195
+ before(:each) do
196
+ @mock_ssh = double(:ssh)
197
+ allow(instance).to receive(:ssh).and_return(@mock_ssh)
198
+ end
199
+
200
+ it 'should try to create a directory remotely' do
201
+ expect(@mock_ssh).to receive(:exec).with(/remote_dir/).and_return(true)
202
+
203
+ instance.send(:ensure_remote_dir, 'testhost', 'remote_dir').should == true
204
+ end
205
+
206
+ it 'should try to create a directory once for each distinct host' do
207
+ expect(@mock_ssh).to receive(:exec).with(/remote_dir/).twice.and_return(true)
208
+
209
+ instance.send(:ensure_remote_dir, 'test-host', 'remote_dir').should == true
210
+ instance.send(:ensure_remote_dir, 'another-host', 'remote_dir').should == true
211
+ end
212
+
213
+ it 'should only try to create a directory once for the same host' do
214
+ expect(@mock_ssh).to receive(:exec).with(/remote_dir/).once.and_return(true)
215
+
216
+ instance.send(:ensure_remote_dir, 'test-host', 'remote_dir').should == true
217
+ instance.send(:ensure_remote_dir, 'test-host', 'remote_dir').should == true
218
+ end
219
+ end
220
+ end
221
+ end
@@ -0,0 +1,181 @@
1
+ require 'spec_helper'
2
+
3
+ describe Harrison::Deploy do
4
+ let(:instance) do
5
+ Harrison::Deploy.new.tap do |d|
6
+ d.hosts = [ 'hf_host' ]
7
+ d.base_dir = '/hf_basedir'
8
+ end
9
+ end
10
+
11
+ describe '.initialize' do
12
+ it 'should add --hosts to arg_opts' do
13
+ instance.instance_variable_get('@arg_opts').to_s.should include(':hosts')
14
+ end
15
+
16
+ it 'should add --env to arg_opts' do
17
+ instance.instance_variable_get('@arg_opts').to_s.should include(':env')
18
+ end
19
+
20
+ it 'should persist options' do
21
+ instance = Harrison::Deploy.new(testopt: 'foo')
22
+
23
+ instance.instance_variable_get('@options').should include(testopt: 'foo')
24
+ end
25
+ end
26
+
27
+ describe 'instance methods' do
28
+ describe '#parse' do
29
+ it 'should require an artifact to be passed in ARGV' do
30
+ output = capture(:stderr) do
31
+ lambda { instance.parse(%w(deploy)) }.should exit_with_code(1)
32
+ end
33
+
34
+ output.should include('must', 'specify', 'artifact')
35
+ end
36
+
37
+ it 'should use "base_dir" from Harrisonfile if present' do
38
+ instance.parse(%w(deploy test_artifact.tar.gz))
39
+
40
+ instance.options.should include({ base_dir: '/hf_basedir' })
41
+ end
42
+ end
43
+
44
+ describe '#remote_exec' do
45
+ before(:each) do
46
+ @mock_ssh = double(:ssh)
47
+ expect(instance).to receive(:ssh).and_return(@mock_ssh)
48
+ end
49
+
50
+ it 'should prepend project dir onto passed command' do
51
+ instance.base_dir = '/opt'
52
+ instance.project = 'test_project'
53
+
54
+ expect(@mock_ssh).to receive(:exec).with("cd /opt/test_project && test_command").and_return('')
55
+
56
+ instance.remote_exec("test_command")
57
+ end
58
+ end
59
+
60
+ describe '#run' do
61
+ before(:each) do
62
+ instance.artifact = 'test_artifact.tar.gz'
63
+ instance.project = 'test_project'
64
+ end
65
+
66
+ context 'when passed a block' do
67
+ it 'should store the block' do
68
+ test_block = Proc.new { |test| "block_output" }
69
+ instance.run(&test_block)
70
+
71
+ instance.instance_variable_get("@run_block").should == test_block
72
+ end
73
+ end
74
+
75
+ context 'when not passed a block' do
76
+ before(:each) do
77
+ @mock_ssh = double(:ssh, exec: '', upload: true, download: true)
78
+ allow(instance).to receive(:ssh).and_return(@mock_ssh)
79
+
80
+ instance.instance_variable_set(:@run_block, Proc.new { |h| "block for #{h.host}" })
81
+ end
82
+
83
+ it 'should use hosts from --hosts if passed' do
84
+ instance.instance_variable_set(:@_argv_hosts, [ 'argv_host1', 'argv_host2' ])
85
+
86
+ output = capture(:stdout) do
87
+ instance.run
88
+ end
89
+
90
+ instance.hosts.should == [ 'argv_host1', 'argv_host2' ]
91
+ output.should include('argv_host1', 'argv_host2')
92
+ output.should_not include('hf_host')
93
+ end
94
+
95
+ it 'should use hosts from Harrisonfile if --hosts not passed' do
96
+ output = capture(:stdout) do
97
+ instance.run
98
+ end
99
+
100
+ instance.hosts.should == [ 'hf_host' ]
101
+ output.should include('hf_host')
102
+ end
103
+
104
+ it 'should require hosts to be set somehow' do
105
+ instance.hosts = nil
106
+
107
+ output = capture(:stderr) do
108
+ lambda { instance.run }.should exit_with_code(1)
109
+ end
110
+
111
+ output.should include('must', 'specify', 'hosts')
112
+ end
113
+
114
+ it 'should invoke the previously stored block once for each host' do
115
+ instance.hosts = [ 'host1', 'host2', 'host3' ]
116
+
117
+ output = capture(:stdout) do
118
+ expect { |b| instance.run(&b); instance.run }.to yield_control.exactly(3).times
119
+ end
120
+
121
+ output.should include('host1', 'host2', 'host3')
122
+ end
123
+ end
124
+ end
125
+
126
+ describe '#close' do
127
+ before(:each) do
128
+ @test_host1_ssh = double(:ssh, 'closed?' => false)
129
+ @test_host2_ssh = double(:ssh, 'closed?' => false)
130
+
131
+ instance.instance_variable_set(:@_conns, { test_host1: @test_host1_ssh, test_host2: @test_host2_ssh })
132
+ end
133
+
134
+ context 'when passed a specific host' do
135
+ it 'should close the connection to that host' do
136
+ expect(@test_host1_ssh).to receive(:close).and_return(true)
137
+
138
+ instance.close(:test_host1)
139
+ end
140
+ end
141
+
142
+ it 'should close every open ssh connection' do
143
+ expect(@test_host1_ssh).to receive(:close).and_return(true)
144
+ expect(@test_host2_ssh).to receive(:close).and_return(true)
145
+
146
+ instance.close
147
+ end
148
+ end
149
+ end
150
+
151
+ describe 'protected methods' do
152
+ describe '#ssh' do
153
+ it 'should open a new SSH connection to self.host' do
154
+ mock_ssh = double(:ssh)
155
+ expect(Harrison::SSH).to receive(:new).and_return(mock_ssh)
156
+
157
+ instance.host = 'test_host'
158
+
159
+ instance.send(:ssh).should == mock_ssh
160
+ end
161
+
162
+ it 'should reuse an existing connection to self.host' do
163
+ mock_ssh = double(:ssh)
164
+ instance.instance_variable_set(:@_conns, { test_host2: mock_ssh })
165
+
166
+ instance.host = :test_host2
167
+
168
+ instance.send(:ssh).should == mock_ssh
169
+ end
170
+ end
171
+
172
+ describe '#remote_project_dir' do
173
+ it 'should combine base_dir and project name' do
174
+ instance.base_dir = '/test_base_dir'
175
+ instance.project = 'test_project'
176
+
177
+ instance.send(:remote_project_dir).should include('/test_base_dir', 'test_project')
178
+ end
179
+ end
180
+ end
181
+ end