harrison 0.2.0 → 0.3.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.
- data/CHANGELOG +23 -0
- data/README.md +62 -9
- data/harrison.gemspec +1 -0
- data/lib/harrison.rb +18 -14
- data/lib/harrison/base.rb +13 -5
- data/lib/harrison/deploy.rb +160 -42
- data/lib/harrison/deploy/phase.rb +51 -0
- data/lib/harrison/package.rb +18 -10
- data/lib/harrison/ssh.rb +12 -6
- data/lib/harrison/version.rb +1 -1
- data/spec/spec_helper.rb +3 -0
- data/spec/unit/harrison/base_spec.rb +4 -13
- data/spec/unit/harrison/deploy/phase_spec.rb +152 -0
- data/spec/unit/harrison/deploy_spec.rb +352 -63
- data/spec/unit/harrison_spec.rb +13 -11
- metadata +22 -3
@@ -0,0 +1,51 @@
|
|
1
|
+
module Harrison
|
2
|
+
class Deploy::Phase
|
3
|
+
attr_accessor :name
|
4
|
+
|
5
|
+
def initialize(name, &phase_config)
|
6
|
+
self.name = name
|
7
|
+
|
8
|
+
@conditions = Array.new
|
9
|
+
|
10
|
+
yield self if block_given?
|
11
|
+
end
|
12
|
+
|
13
|
+
def add_condition(&block)
|
14
|
+
@conditions << block
|
15
|
+
end
|
16
|
+
|
17
|
+
# Ensure all conditions eval to true for this context.
|
18
|
+
def matches_context?(context)
|
19
|
+
@conditions.all? { |cblock| cblock.call(context) }
|
20
|
+
end
|
21
|
+
|
22
|
+
def on_run(&block)
|
23
|
+
@run_block = block
|
24
|
+
end
|
25
|
+
|
26
|
+
def on_fail(&block)
|
27
|
+
@fail_block = block
|
28
|
+
end
|
29
|
+
|
30
|
+
# These should only be invoked by the deploy action.
|
31
|
+
def _run(context)
|
32
|
+
return unless matches_context?(context)
|
33
|
+
|
34
|
+
if @run_block
|
35
|
+
puts "[#{context.host}] Executing \"#{self.name}\"..."
|
36
|
+
@run_block.call(context)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def _fail(context)
|
41
|
+
# Ensure all conditions eval to true for this context.
|
42
|
+
return unless matches_context?(context)
|
43
|
+
|
44
|
+
if @fail_block
|
45
|
+
puts "[#{context.host}] Reverting \"#{self.name}\"..."
|
46
|
+
@fail_block.call(context)
|
47
|
+
end
|
48
|
+
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
data/lib/harrison/package.rb
CHANGED
@@ -23,7 +23,11 @@ module Harrison
|
|
23
23
|
def remote_exec(cmd)
|
24
24
|
ensure_remote_dir("#{remote_project_dir}/package")
|
25
25
|
|
26
|
-
|
26
|
+
if @_remote_context
|
27
|
+
super("cd #{@_remote_context} && #{cmd}")
|
28
|
+
else
|
29
|
+
super("cd #{remote_project_dir}/package && #{cmd}")
|
30
|
+
end
|
27
31
|
end
|
28
32
|
|
29
33
|
def run(&block)
|
@@ -40,18 +44,22 @@ module Harrison
|
|
40
44
|
# Fetch/clone git repo on remote host.
|
41
45
|
remote_exec("if [ -d cached ] ; then cd cached && git fetch origin -p ; else git clone #{git_src} cached ; fi")
|
42
46
|
|
43
|
-
# Check out target commit.
|
44
|
-
remote_exec("cd cached && git reset --hard #{commit} && git clean -f -d")
|
45
|
-
|
46
47
|
# Make a build folder of the target commit.
|
47
|
-
remote_exec("rm -rf #{commit} && cp -a cached #{commit}")
|
48
|
+
remote_exec("rm -rf #{artifact_name(commit)} && cp -a cached #{artifact_name(commit)}")
|
48
49
|
|
49
|
-
#
|
50
|
-
#
|
51
|
-
|
50
|
+
# Check out target commit.
|
51
|
+
remote_exec("cd #{artifact_name(commit)} && git reset --hard #{commit} && git clean -f -d")
|
52
|
+
|
53
|
+
# Run user supplied build code in the context of the checked out code.
|
54
|
+
begin
|
55
|
+
@_remote_context = "#{remote_project_dir}/package/#{artifact_name(commit)}"
|
56
|
+
super
|
57
|
+
ensure
|
58
|
+
@_remote_context = nil
|
59
|
+
end
|
52
60
|
|
53
61
|
# Package build folder into tgz.
|
54
|
-
remote_exec("rm -f #{artifact_name(commit)}.tar.gz && cd #{commit} && tar #{excludes_for_tar} -czf ../#{artifact_name(commit)}.tar.gz .")
|
62
|
+
remote_exec("rm -f #{artifact_name(commit)}.tar.gz && cd #{artifact_name(commit)} && tar #{excludes_for_tar} -czf ../#{artifact_name(commit)}.tar.gz .")
|
55
63
|
|
56
64
|
if match = remote_regex.match(destination)
|
57
65
|
# Copy artifact to remote destination.
|
@@ -65,7 +73,7 @@ module Harrison
|
|
65
73
|
end
|
66
74
|
|
67
75
|
if purge
|
68
|
-
remote_exec("
|
76
|
+
remote_exec("rm -rf #{artifact_name(commit)}")
|
69
77
|
end
|
70
78
|
|
71
79
|
puts "Sucessfully packaged #{commit} to #{destination}/#{artifact_name(commit)}.tar.gz"
|
data/lib/harrison/ssh.rb
CHANGED
@@ -14,26 +14,32 @@ module Harrison
|
|
14
14
|
end
|
15
15
|
|
16
16
|
def exec(command)
|
17
|
-
puts "INFO
|
17
|
+
puts "[#{desc}] INFO: ssh-exec #{command}" if Harrison::DEBUG
|
18
18
|
|
19
19
|
result = invoke(@conn, command)
|
20
20
|
|
21
21
|
if Harrison::DEBUG || result[:status] != 0
|
22
|
-
warn "
|
23
|
-
warn "
|
22
|
+
warn "[#{desc}] STDERR: #{result[:stderr]}" unless result[:stderr].empty?
|
23
|
+
warn "[#{desc}] STDOUT: #{result[:stdout]}" unless result[:stdout].empty?
|
24
24
|
end
|
25
25
|
|
26
26
|
(result[:status] == 0) ? result[:stdout] : nil
|
27
27
|
end
|
28
28
|
|
29
29
|
def download(remote_path, local_path)
|
30
|
-
puts "INFO
|
30
|
+
puts "[#{desc}] INFO: scp-down #{local_path} <<< #{remote_path}" if Harrison::DEBUG
|
31
|
+
|
31
32
|
@conn.scp.download!(remote_path, local_path)
|
33
|
+
|
34
|
+
return true
|
32
35
|
end
|
33
36
|
|
34
37
|
def upload(local_path, remote_path)
|
35
|
-
puts "INFO
|
38
|
+
puts "[#{desc}] INFO: scp-up #{local_path} >>> #{remote_path}" if Harrison::DEBUG
|
39
|
+
|
36
40
|
@conn.scp.upload!(local_path, remote_path)
|
41
|
+
|
42
|
+
return true
|
37
43
|
end
|
38
44
|
|
39
45
|
def close
|
@@ -68,7 +74,7 @@ module Harrison
|
|
68
74
|
|
69
75
|
conn.open_channel do |channel|
|
70
76
|
channel.exec(cmd) do |ch, success|
|
71
|
-
warn "Couldn't execute command (ssh.channel.exec)
|
77
|
+
warn "[#{conn.host}] Couldn't execute command (ssh.channel.exec): #{cmd}" unless success
|
72
78
|
|
73
79
|
channel.on_data do |ch,data|
|
74
80
|
stdout_data += data
|
data/lib/harrison/version.rb
CHANGED
data/spec/spec_helper.rb
CHANGED
@@ -43,12 +43,8 @@ describe Harrison::Base do
|
|
43
43
|
expect(instance.exec('echo "foo"')).to eq('foo')
|
44
44
|
end
|
45
45
|
|
46
|
-
it 'should
|
47
|
-
|
48
|
-
expect(lambda { instance.exec('cat noexist 2>/dev/null') }).to exit_with_code(1)
|
49
|
-
end
|
50
|
-
|
51
|
-
expect(output).to include('unable', 'execute', 'local', 'command')
|
46
|
+
it 'should throw :failure if command returns non-zero' do
|
47
|
+
expect(lambda { instance.exec('cat noexist 2>/dev/null') }).to throw_symbol(:failure)
|
52
48
|
end
|
53
49
|
end
|
54
50
|
|
@@ -64,14 +60,9 @@ describe Harrison::Base do
|
|
64
60
|
expect(instance.remote_exec('remote exec')).to eq('remote_exec_return')
|
65
61
|
end
|
66
62
|
|
67
|
-
it 'should
|
63
|
+
it 'should throw :failure if command returns nil' do
|
68
64
|
expect(@mock_ssh).to receive(:exec).and_return(nil)
|
69
|
-
|
70
|
-
output = capture(:stderr) do
|
71
|
-
expect(lambda { instance.remote_exec('remote exec fail') }).to exit_with_code(1)
|
72
|
-
end
|
73
|
-
|
74
|
-
expect(output).to include('unable', 'execute', 'remote', 'command')
|
65
|
+
expect(lambda { instance.remote_exec('remote exec fail') }).to throw_symbol(:failure)
|
75
66
|
end
|
76
67
|
end
|
77
68
|
|
@@ -0,0 +1,152 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Harrison::Deploy::Phase do
|
4
|
+
let(:instance) do
|
5
|
+
Harrison::Deploy::Phase.new(:test_phase)
|
6
|
+
end
|
7
|
+
|
8
|
+
describe 'initialize' do
|
9
|
+
it 'should set name' do
|
10
|
+
expect(instance.name).to be :test_phase
|
11
|
+
end
|
12
|
+
|
13
|
+
it 'should yield itself to passed block' do
|
14
|
+
expect { |b| Harrison::Deploy::Phase.new(:block_test, &b) }.to yield_with_args(Harrison::Deploy::Phase)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
describe 'instance methods' do
|
19
|
+
describe '#add_condition' do
|
20
|
+
it 'should accept and store a block' do
|
21
|
+
instance.add_condition { |c| true }
|
22
|
+
|
23
|
+
expect(instance.instance_variable_get(:@conditions).size).to be 1
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
describe '#matches_context?' do
|
28
|
+
it 'should pass the given context to each condition block' do
|
29
|
+
mock_proc = double(:proc)
|
30
|
+
instance.instance_variable_set(:@conditions, [ mock_proc ])
|
31
|
+
|
32
|
+
mock_context = double(:context)
|
33
|
+
|
34
|
+
expect(mock_proc).to receive(:call).with(mock_context)
|
35
|
+
|
36
|
+
instance.matches_context?(mock_context)
|
37
|
+
end
|
38
|
+
|
39
|
+
it 'should be true if all conditions evaluate to true' do
|
40
|
+
instance.instance_variable_set(:@conditions, [
|
41
|
+
Proc.new { |c| true },
|
42
|
+
Proc.new { |c| true },
|
43
|
+
Proc.new { |c| true },
|
44
|
+
])
|
45
|
+
|
46
|
+
expect(instance.matches_context?(double(:context))).to be true
|
47
|
+
end
|
48
|
+
|
49
|
+
it 'should be false if at least one condition evaluates to false' do
|
50
|
+
instance.instance_variable_set(:@conditions, [
|
51
|
+
Proc.new { |c| true },
|
52
|
+
Proc.new { |c| false },
|
53
|
+
Proc.new { |c| true },
|
54
|
+
])
|
55
|
+
|
56
|
+
expect(instance.matches_context?(double(:context))).to be false
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
describe '#on_run' do
|
61
|
+
it 'should store the given block' do
|
62
|
+
instance.on_run { |c| true }
|
63
|
+
|
64
|
+
expect(instance.instance_variable_get(:@run_block)).to_not be_nil
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
describe '#on_fail' do
|
69
|
+
it 'should store the given block' do
|
70
|
+
instance.on_fail { |c| true }
|
71
|
+
|
72
|
+
expect(instance.instance_variable_get(:@fail_block)).to_not be_nil
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
describe '#_run' do
|
77
|
+
context 'context matches conditions' do
|
78
|
+
before(:each) do
|
79
|
+
allow(instance).to receive(:matches_context?).and_return(true)
|
80
|
+
end
|
81
|
+
|
82
|
+
it 'should invoke the previously stored block with given context' do
|
83
|
+
mock_context = double(:context, host: 'test_host')
|
84
|
+
|
85
|
+
mock_proc = double(:proc)
|
86
|
+
instance.instance_variable_set(:@run_block, mock_proc)
|
87
|
+
|
88
|
+
expect(mock_proc).to receive(:call).with(mock_context)
|
89
|
+
|
90
|
+
capture(:stdout) do
|
91
|
+
instance._run(mock_context)
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
context 'context does not match conditions' do
|
97
|
+
before(:each) do
|
98
|
+
allow(instance).to receive(:matches_context?).and_return(false)
|
99
|
+
end
|
100
|
+
|
101
|
+
it 'should not invoke the previously stored block' do
|
102
|
+
mock_context = double(:context)
|
103
|
+
|
104
|
+
mock_proc = double(:proc)
|
105
|
+
instance.instance_variable_set(:@run_block, mock_proc)
|
106
|
+
|
107
|
+
expect(mock_proc).to_not receive(:call)
|
108
|
+
|
109
|
+
instance._run(mock_context)
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
describe '#_fail' do
|
115
|
+
context 'context matches conditions' do
|
116
|
+
before(:each) do
|
117
|
+
allow(instance).to receive(:matches_context?).and_return(true)
|
118
|
+
end
|
119
|
+
|
120
|
+
it 'should invoke the previously stored block with given context' do
|
121
|
+
mock_context = double(:context, host: 'test_host')
|
122
|
+
|
123
|
+
mock_proc = double(:proc)
|
124
|
+
instance.instance_variable_set(:@fail_block, mock_proc)
|
125
|
+
|
126
|
+
expect(mock_proc).to receive(:call).with(mock_context)
|
127
|
+
|
128
|
+
capture(:stdout) do
|
129
|
+
instance._fail(mock_context)
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
context 'context does not match conditions' do
|
135
|
+
before(:each) do
|
136
|
+
allow(instance).to receive(:matches_context?).and_return(false)
|
137
|
+
end
|
138
|
+
|
139
|
+
it 'should not invoke the previously stored block' do
|
140
|
+
mock_context = double(:context)
|
141
|
+
|
142
|
+
mock_proc = double(:proc)
|
143
|
+
instance.instance_variable_set(:@fail_block, mock_proc)
|
144
|
+
|
145
|
+
expect(mock_proc).to_not receive(:call)
|
146
|
+
|
147
|
+
instance._fail(mock_context)
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|
@@ -22,6 +22,10 @@ describe Harrison::Deploy do
|
|
22
22
|
|
23
23
|
expect(instance.instance_variable_get('@options')).to include(testopt: 'foo')
|
24
24
|
end
|
25
|
+
|
26
|
+
it 'should set up default phases' do
|
27
|
+
expect(instance.instance_variable_get(:@_phases)).to_not be_empty
|
28
|
+
end
|
25
29
|
end
|
26
30
|
|
27
31
|
describe 'instance methods' do
|
@@ -41,121 +45,249 @@ describe Harrison::Deploy do
|
|
41
45
|
end
|
42
46
|
end
|
43
47
|
|
48
|
+
describe '#add_phase' do
|
49
|
+
before(:each) do
|
50
|
+
allow(Harrison::Deploy::Phase).to receive(:new).with(anything)
|
51
|
+
end
|
52
|
+
|
53
|
+
it 'should instantiate a new Phase object with the given name' do
|
54
|
+
expect(Harrison::Deploy::Phase).to receive(:new).with(:test)
|
55
|
+
|
56
|
+
instance.add_phase(:test)
|
57
|
+
end
|
58
|
+
|
59
|
+
it 'should pass a given block to the Phase object constructor' do
|
60
|
+
@mock_phase = double(:phase)
|
61
|
+
|
62
|
+
expect(Harrison::Deploy::Phase).to receive(:new).with(:test).and_yield(@mock_phase)
|
63
|
+
expect(@mock_phase).to receive(:in_block)
|
64
|
+
|
65
|
+
instance.add_phase :test do |p|
|
66
|
+
p.in_block
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
44
71
|
describe '#remote_exec' do
|
45
72
|
before(:each) do
|
73
|
+
instance.base_dir = '/opt'
|
74
|
+
instance.project = 'test_project'
|
75
|
+
|
46
76
|
@mock_ssh = double(:ssh)
|
47
77
|
expect(instance).to receive(:ssh).and_return(@mock_ssh)
|
48
78
|
end
|
49
79
|
|
50
80
|
it 'should prepend project dir onto passed command' do
|
51
|
-
instance.base_dir = '/opt'
|
52
|
-
instance.project = 'test_project'
|
53
|
-
|
54
81
|
expect(@mock_ssh).to receive(:exec).with("cd /opt/test_project && test_command").and_return('')
|
55
82
|
|
56
83
|
instance.remote_exec("test_command")
|
57
84
|
end
|
58
85
|
end
|
59
86
|
|
60
|
-
describe '#
|
87
|
+
describe '#current_symlink' do
|
88
|
+
it 'starts at base_dir' do
|
89
|
+
expect(instance.current_symlink).to match(/^#{instance.base_dir}/)
|
90
|
+
end
|
91
|
+
|
92
|
+
it 'is not just base_dir' do
|
93
|
+
expect(instance.current_symlink).to_not equal(instance.base_dir)
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
describe '#update_current_symlink' do
|
61
98
|
before(:each) do
|
62
|
-
instance.
|
63
|
-
|
99
|
+
instance.deploy_link = 'new_deploy'
|
100
|
+
|
101
|
+
allow(instance).to receive(:remote_exec).with(/^ln/)
|
64
102
|
end
|
65
103
|
|
66
|
-
context '
|
67
|
-
|
68
|
-
|
69
|
-
|
104
|
+
context 'current_symlink already exists' do
|
105
|
+
before(:each) do
|
106
|
+
expect(instance).to receive(:remote_exec).with(/readlink/).and_return('old_link_target')
|
107
|
+
end
|
108
|
+
|
109
|
+
it 'should store old symlink target' do
|
110
|
+
instance.update_current_symlink
|
70
111
|
|
71
|
-
expect(instance.instance_variable_get(
|
112
|
+
expect(instance.instance_variable_get(:@_old_current)).to_not be_nil
|
113
|
+
end
|
114
|
+
|
115
|
+
it 'should replace existing symlink' do
|
116
|
+
expect(instance).to receive(:remote_exec).with(/^ln .* #{instance.deploy_link}/)
|
117
|
+
|
118
|
+
instance.update_current_symlink
|
72
119
|
end
|
73
120
|
end
|
74
121
|
|
75
|
-
context '
|
122
|
+
context 'current_symlink does not exist yet' do
|
76
123
|
before(:each) do
|
77
|
-
|
78
|
-
|
124
|
+
expect(instance).to receive(:remote_exec).with(/readlink/).and_return('')
|
125
|
+
end
|
126
|
+
|
127
|
+
it 'should not store old symlink target' do
|
128
|
+
instance.update_current_symlink
|
79
129
|
|
80
|
-
instance.
|
130
|
+
expect(instance.instance_variable_get(:@_old_current)).to be_nil
|
81
131
|
end
|
82
132
|
|
83
|
-
it 'should
|
84
|
-
instance.
|
133
|
+
it 'should create symlink' do
|
134
|
+
expect(instance).to receive(:remote_exec).with(/^ln .* #{instance.deploy_link}/)
|
85
135
|
|
86
|
-
|
87
|
-
|
88
|
-
|
136
|
+
instance.update_current_symlink
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
describe '#revert_current_symlink' do
|
142
|
+
it 'should set link back to old target if old target is set' do
|
143
|
+
instance.instance_variable_set(:@_old_current, 'old_link_target')
|
144
|
+
|
145
|
+
expect(instance).to receive(:remote_exec).with(/^ln .* old_link_target/)
|
146
|
+
|
147
|
+
instance.revert_current_symlink
|
148
|
+
end
|
149
|
+
|
150
|
+
it 'should be a no-op if old target is not set' do
|
151
|
+
expect(instance).to_not receive(:remote_exec)
|
152
|
+
|
153
|
+
instance.revert_current_symlink
|
154
|
+
end
|
155
|
+
end
|
89
156
|
|
90
|
-
|
91
|
-
|
92
|
-
|
157
|
+
describe '#run' do
|
158
|
+
before(:each) do
|
159
|
+
instance.artifact = 'test_artifact.tar.gz'
|
160
|
+
instance.project = 'test_project'
|
161
|
+
|
162
|
+
@mock_ssh = double(:ssh, host: 'test_host1', exec: '', upload: true, download: true)
|
163
|
+
allow(instance).to receive(:ssh).and_return(@mock_ssh)
|
164
|
+
end
|
165
|
+
|
166
|
+
it 'should use hosts from --hosts if passed' do
|
167
|
+
instance.instance_variable_set(:@_argv_hosts, [ 'argv_host1', 'argv_host2' ])
|
168
|
+
|
169
|
+
output = capture(:stdout) do
|
170
|
+
instance.run
|
93
171
|
end
|
94
172
|
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
173
|
+
expect(instance.hosts).to eq([ 'argv_host1', 'argv_host2' ])
|
174
|
+
expect(output).to include('argv_host1', 'argv_host2')
|
175
|
+
expect(output).to_not include('hf_host')
|
176
|
+
end
|
99
177
|
|
100
|
-
|
101
|
-
|
178
|
+
it 'should use hosts from Harrisonfile if --hosts not passed' do
|
179
|
+
output = capture(:stdout) do
|
180
|
+
instance.run
|
102
181
|
end
|
103
182
|
|
104
|
-
|
105
|
-
|
183
|
+
expect(instance.hosts).to eq([ 'hf_host' ])
|
184
|
+
expect(output).to include('hf_host')
|
185
|
+
end
|
106
186
|
|
107
|
-
|
108
|
-
|
109
|
-
end
|
187
|
+
it 'should require hosts to be set somehow' do
|
188
|
+
instance.hosts = nil
|
110
189
|
|
111
|
-
|
190
|
+
output = capture(:stderr) do
|
191
|
+
expect(lambda { instance.run }).to exit_with_code(1)
|
112
192
|
end
|
113
193
|
|
114
|
-
|
115
|
-
|
194
|
+
expect(output).to include('must', 'specify', 'hosts')
|
195
|
+
end
|
116
196
|
|
117
|
-
|
118
|
-
|
119
|
-
|
197
|
+
it 'should run the specified phases once for each host' do
|
198
|
+
instance.hosts = [ 'host1', 'host2', 'host3' ]
|
199
|
+
|
200
|
+
mock_phase = double(:phase, matches_context?: true)
|
201
|
+
expect(Harrison::Deploy::Phase).to receive(:new).with(:test).and_return(mock_phase)
|
202
|
+
instance.add_phase :test
|
203
|
+
instance.phases = [ :test ]
|
204
|
+
|
205
|
+
expect(mock_phase).to receive(:_run).exactly(3).times
|
120
206
|
|
121
|
-
|
207
|
+
output = capture(:stdout) do
|
208
|
+
instance.run
|
122
209
|
end
|
123
210
|
|
124
|
-
|
125
|
-
|
211
|
+
expect(output).to include('host1', 'host2', 'host3')
|
212
|
+
end
|
126
213
|
|
127
|
-
|
128
|
-
|
214
|
+
context 'when a deployment phase fails on a host' do
|
215
|
+
before(:each) do
|
216
|
+
@upload = double(:phase, name: :upload, matches_context?: true, _run: true, _fail: true)
|
217
|
+
@extract = double(:phase, name: :extract, matches_context?: true, _run: true, _fail: true)
|
218
|
+
@link = double(:phase, name: :link, matches_context?: true)
|
129
219
|
|
130
|
-
|
131
|
-
|
220
|
+
allow(@link).to receive(:_run).and_throw(:failure, true)
|
221
|
+
|
222
|
+
instance.instance_variable_set(:@_phases, {
|
223
|
+
upload: @upload,
|
224
|
+
extract: @extract,
|
225
|
+
link: @link,
|
226
|
+
})
|
227
|
+
|
228
|
+
instance.phases = [ :upload, :extract, :link ]
|
229
|
+
|
230
|
+
instance.hosts = [ 'host1', 'host2', 'host3' ]
|
231
|
+
end
|
232
|
+
|
233
|
+
it "should invoke on_fail block for completed phases on each host" do
|
234
|
+
expect(@extract).to receive(:_fail).exactly(3).times.ordered
|
235
|
+
expect(@upload).to receive(:_fail).exactly(3).times.ordered
|
236
|
+
|
237
|
+
capture([ :stdout, :stderr ]) do
|
238
|
+
expect(lambda { instance.run }).to exit_with_code(1)
|
132
239
|
end
|
133
240
|
end
|
134
241
|
|
135
|
-
|
136
|
-
|
137
|
-
|
242
|
+
it "should invoke on_fail block on each host in reverse order" do
|
243
|
+
expect(@extract).to receive(:_fail) { |context| expect(context.host).to eq('host3') }.ordered
|
244
|
+
expect(@extract).to receive(:_fail) { |context| expect(context.host).to eq('host2') }.ordered
|
245
|
+
expect(@extract).to receive(:_fail) { |context| expect(context.host).to eq('host1') }.ordered
|
246
|
+
|
247
|
+
capture([ :stdout, :stderr ]) do
|
248
|
+
expect(lambda { instance.run }).to exit_with_code(1)
|
138
249
|
end
|
250
|
+
end
|
251
|
+
end
|
139
252
|
|
140
|
-
|
141
|
-
|
142
|
-
|
253
|
+
context 'when invoked via rollback' do
|
254
|
+
before(:each) do
|
255
|
+
instance.rollback = true
|
143
256
|
|
144
|
-
|
145
|
-
instance.run
|
146
|
-
end
|
257
|
+
instance.project = 'test_project'
|
147
258
|
|
148
|
-
|
259
|
+
@mock_ssh = double(:ssh, host: 'test_host1', exec: '', upload: true, download: true)
|
260
|
+
allow(instance).to receive(:ssh).and_return(@mock_ssh)
|
261
|
+
end
|
262
|
+
|
263
|
+
it 'should find the release of the previous deploy' do
|
264
|
+
expect(instance).to receive(:deploys).and_return([ 'deploy_1', 'deploy_2', 'deploy_3', 'deploy_4', 'deploy_5' ])
|
265
|
+
expect(@mock_ssh).to receive(:exec).with(/readlink .* deploy_4/)
|
266
|
+
|
267
|
+
capture(:stdout) do
|
268
|
+
instance.run
|
149
269
|
end
|
270
|
+
end
|
150
271
|
|
151
|
-
|
152
|
-
|
272
|
+
it 'should not run :upload, :extract, or :cleanup phases' do
|
273
|
+
expect(instance).to receive(:deploys).and_return([ 'deploy_1', 'deploy_2', 'deploy_3', 'deploy_4', 'deploy_5' ])
|
274
|
+
expect(@mock_ssh).to receive(:exec).with(/readlink .* deploy_4/).and_return('old_release')
|
153
275
|
|
154
|
-
|
155
|
-
|
156
|
-
|
276
|
+
disabled_phase = double(:phase)
|
277
|
+
expect(disabled_phase).to_not receive(:_run)
|
278
|
+
|
279
|
+
enabled_phase = double(:phase, matches_context?: true)
|
280
|
+
expect(enabled_phase).to receive(:_run)
|
157
281
|
|
158
|
-
|
282
|
+
instance.instance_variable_set(:@_phases, {
|
283
|
+
upload: disabled_phase,
|
284
|
+
extract: disabled_phase,
|
285
|
+
link: enabled_phase,
|
286
|
+
cleanup: disabled_phase,
|
287
|
+
})
|
288
|
+
|
289
|
+
capture(:stdout) do
|
290
|
+
instance.run
|
159
291
|
end
|
160
292
|
end
|
161
293
|
end
|
@@ -220,6 +352,20 @@ describe Harrison::Deploy do
|
|
220
352
|
end
|
221
353
|
|
222
354
|
describe 'protected methods' do
|
355
|
+
describe '#add_default_phases' do
|
356
|
+
it 'should add each default phase' do
|
357
|
+
# Trigger constructor invocations so we can only count new invocation below.
|
358
|
+
instance
|
359
|
+
|
360
|
+
expect(Harrison::Deploy::Phase).to receive(:new).with(:upload)
|
361
|
+
expect(Harrison::Deploy::Phase).to receive(:new).with(:extract)
|
362
|
+
expect(Harrison::Deploy::Phase).to receive(:new).with(:link)
|
363
|
+
expect(Harrison::Deploy::Phase).to receive(:new).with(:cleanup)
|
364
|
+
|
365
|
+
instance.send(:add_default_phases)
|
366
|
+
end
|
367
|
+
end
|
368
|
+
|
223
369
|
describe '#ssh' do
|
224
370
|
it 'should open a new SSH connection to self.host' do
|
225
371
|
mock_ssh = double(:ssh)
|
@@ -316,4 +462,147 @@ describe Harrison::Deploy do
|
|
316
462
|
end
|
317
463
|
end
|
318
464
|
end
|
465
|
+
|
466
|
+
describe 'default phases' do
|
467
|
+
before(:each) do
|
468
|
+
instance.artifact = '/tmp/test_artifact.tar.gz'
|
469
|
+
|
470
|
+
@mock_ssh = double(:ssh, host: 'test_host1', exec: '', upload: true, download: true)
|
471
|
+
allow(instance).to receive(:ssh).and_return(@mock_ssh)
|
472
|
+
end
|
473
|
+
|
474
|
+
describe 'upload' do
|
475
|
+
before(:each) do
|
476
|
+
@phase = instance.instance_variable_get(:@_phases)[:upload]
|
477
|
+
end
|
478
|
+
|
479
|
+
describe 'on_run' do
|
480
|
+
context 'when deploying from a local artifact' do
|
481
|
+
it 'should invoke Harrison::SSH.upload' do
|
482
|
+
expect(@mock_ssh).to receive(:upload).with(/test_artifact\.tar\.gz/, anything)
|
483
|
+
|
484
|
+
capture(:stdout) do
|
485
|
+
@phase._run(instance)
|
486
|
+
end
|
487
|
+
end
|
488
|
+
end
|
489
|
+
|
490
|
+
context 'when deploying from a remote artifact' do
|
491
|
+
before(:each) do
|
492
|
+
instance.artifact = 'test_user@test_host1:/tmp/test_artifact.tar.gz'
|
493
|
+
end
|
494
|
+
|
495
|
+
it 'should invoke scp on the remote host' do
|
496
|
+
allow(instance).to receive(:remote_exec).and_return('')
|
497
|
+
expect(instance).to receive(:remote_exec).with(/scp test_user@test_host1:\/tmp\/test_artifact.tar.gz/).and_return('')
|
498
|
+
|
499
|
+
capture(:stdout) do
|
500
|
+
@phase._run(instance)
|
501
|
+
end
|
502
|
+
end
|
503
|
+
|
504
|
+
it 'should not invoke Harrison::SSH.upload' do
|
505
|
+
expect(@mock_ssh).not_to receive(:upload)
|
506
|
+
|
507
|
+
capture(:stdout) do
|
508
|
+
@phase._run(instance)
|
509
|
+
end
|
510
|
+
end
|
511
|
+
end
|
512
|
+
end
|
513
|
+
|
514
|
+
describe 'on_fail' do
|
515
|
+
it 'should remove uploaded artifact' do
|
516
|
+
expect(instance).to receive(:remote_exec).with(/rm.*test_artifact\.tar\.gz/)
|
517
|
+
|
518
|
+
capture(:stdout) do
|
519
|
+
@phase._fail(instance)
|
520
|
+
end
|
521
|
+
end
|
522
|
+
end
|
523
|
+
end
|
524
|
+
|
525
|
+
describe 'extract' do
|
526
|
+
before(:each) do
|
527
|
+
@phase = instance.instance_variable_get(:@_phases)[:extract]
|
528
|
+
end
|
529
|
+
|
530
|
+
describe 'on_run' do
|
531
|
+
it 'should untar the artifact and then remove it' do
|
532
|
+
allow(instance).to receive(:remote_exec)
|
533
|
+
expect(instance).to receive(:remote_exec).with(/tar.*test_artifact\.tar\.gz/).ordered
|
534
|
+
expect(instance).to receive(:remote_exec).with(/rm.*test_artifact\.tar\.gz/).ordered
|
535
|
+
|
536
|
+
capture(:stdout) do
|
537
|
+
@phase._run(instance)
|
538
|
+
end
|
539
|
+
end
|
540
|
+
end
|
541
|
+
|
542
|
+
describe 'on_fail' do
|
543
|
+
it 'should remove the extracted release' do
|
544
|
+
expect(instance).to receive(:remote_exec).with(/rm.*#{instance.release_dir}/)
|
545
|
+
|
546
|
+
capture(:stdout) do
|
547
|
+
@phase._fail(instance)
|
548
|
+
end
|
549
|
+
end
|
550
|
+
end
|
551
|
+
end
|
552
|
+
|
553
|
+
describe 'link' do
|
554
|
+
before(:each) do
|
555
|
+
@phase = instance.instance_variable_get(:@_phases)[:link]
|
556
|
+
end
|
557
|
+
|
558
|
+
describe 'on_run' do
|
559
|
+
it 'should create a new deploy link' do
|
560
|
+
expect(instance).to receive(:remote_exec).with(/ln.*#{instance.release_dir}.*#{instance.deploy_link}/)
|
561
|
+
|
562
|
+
capture(:stdout) do
|
563
|
+
@phase._run(instance)
|
564
|
+
end
|
565
|
+
end
|
566
|
+
end
|
567
|
+
|
568
|
+
describe 'on_fail' do
|
569
|
+
it 'should remove deploy link' do
|
570
|
+
expect(instance).to receive(:remote_exec).with(/rm.*#{instance.deploy_link}/)
|
571
|
+
|
572
|
+
capture(:stdout) do
|
573
|
+
@phase._fail(instance)
|
574
|
+
end
|
575
|
+
end
|
576
|
+
end
|
577
|
+
end
|
578
|
+
|
579
|
+
describe 'cleanup' do
|
580
|
+
before(:each) do
|
581
|
+
@phase = instance.instance_variable_get(:@_phases)[:cleanup]
|
582
|
+
end
|
583
|
+
|
584
|
+
describe 'on_run' do
|
585
|
+
it 'should clean up old releases if passed a --keep option' do
|
586
|
+
instance.keep = 3
|
587
|
+
|
588
|
+
expect(instance).to receive(:cleanup_deploys).with(3)
|
589
|
+
expect(instance).to receive(:cleanup_releases)
|
590
|
+
|
591
|
+
capture(:stdout) do
|
592
|
+
@phase._run(instance)
|
593
|
+
end
|
594
|
+
end
|
595
|
+
end
|
596
|
+
|
597
|
+
describe 'on_fail' do
|
598
|
+
it 'should not do anything' do
|
599
|
+
expect(instance).to_not receive(:remote_exec)
|
600
|
+
|
601
|
+
capture(:stdout) do
|
602
|
+
@phase._fail(instance)
|
603
|
+
end
|
604
|
+
end
|
605
|
+
end
|
606
|
+
end
|
607
|
+
end
|
319
608
|
end
|