harrison 0.0.1

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