centurion 1.0.6

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,52 @@
1
+ require 'centurion/docker_registry'
2
+
3
+ task :list do
4
+ invoke 'list:tags'
5
+ invoke 'list:running_containers'
6
+ end
7
+
8
+ namespace :list do
9
+ task :running_container_tags do
10
+
11
+ tags = get_current_tags_for(fetch(:image))
12
+
13
+ $stderr.puts "\n\nCurrent #{current_environment} tags for #{fetch(:image)}:\n\n"
14
+ tags.each do |info|
15
+ if info && !info[:tags].empty?
16
+ $stderr.puts "#{'%-20s' % info[:server]}: #{info[:tags].join(', ')}"
17
+ else
18
+ $stderr.puts "#{'%-20s' % info[:server]}: NO TAGS!"
19
+ end
20
+ end
21
+
22
+ $stderr.puts "\nAll tags for this image: #{tags.map { |t| t[:tags] }.flatten.uniq.join(', ')}"
23
+ end
24
+
25
+ task :tags do
26
+ begin
27
+ registry = Centurion::DockerRegistry.new()
28
+ tags = registry.respository_tags(fetch(:image))
29
+ tags.each do |tag|
30
+ puts "\t#{tag[0]}\t-> #{tag[1][0..11]}"
31
+ end
32
+ rescue StandardError => e
33
+ error "Couldn't communicate with Registry: #{e.message}"
34
+ end
35
+ puts
36
+ end
37
+
38
+ task :running_containers do
39
+ on_each_docker_host do |target_server|
40
+ begin
41
+ running_containers = target_server.ps
42
+ running_containers.each do |container|
43
+ puts container.inspect
44
+ end
45
+ rescue StandardError => e
46
+ error "Couldn't communicate with Docker on #{target_server.hostname}: #{e.message}"
47
+ raise
48
+ end
49
+ puts
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,67 @@
1
+ require 'spec_helper'
2
+ require 'capistrano_dsl'
3
+
4
+ class DSLTest
5
+ extend Capistrano::DSL
6
+ end
7
+
8
+ describe Capistrano::DSL do
9
+ before do
10
+ DSLTest.clear_env
11
+ end
12
+
13
+ context 'handling multiple environments' do
14
+ it 'sets the environment' do
15
+ expect { DSLTest.set_current_environment(:test) }.not_to raise_error
16
+ end
17
+
18
+ it 'fetchs the current environment' do
19
+ DSLTest.set_current_environment(:test)
20
+ expect(DSLTest.current_environment).to eq(:test)
21
+ end
22
+ end
23
+
24
+ context 'without a current environment set' do
25
+ it 'dies if the current_environment is not set' do
26
+ expect { DSLTest.set(:foo, 'asdf') }.to raise_error(Capistrano::DSL::CurrentEnvironmentNotSetError)
27
+ end
28
+ end
29
+
30
+ context 'with a current environment set' do
31
+ before do
32
+ DSLTest.set_current_environment(:test)
33
+ end
34
+
35
+ it 'stores variables in the environment' do
36
+ expect { DSLTest.set(:foo, 'bar') }.not_to raise_error
37
+ expect(DSLTest).to have_key_and_value(:foo, 'bar')
38
+ end
39
+
40
+ it 'deletes keys from the environment' do
41
+ DSLTest.set(:foo, 'bar')
42
+ expect(DSLTest).to have_key_and_value(:foo, 'bar')
43
+ DSLTest.delete(:foo)
44
+ expect(DSLTest.fetch(:foo)).to be_nil
45
+ end
46
+
47
+ it 'returns true for any? when the value exists' do
48
+ DSLTest.set(:foo, 'bar')
49
+ expect(DSLTest.any?(:foo)).to be_true
50
+ end
51
+
52
+ it 'returns false for any? when the value does not exist' do
53
+ expect(DSLTest.any?(:foo)).to be_false
54
+ end
55
+
56
+ it 'passes through the any? method to values that support it' do
57
+ class NoAny
58
+ def any?
59
+ 'oh no'
60
+ end
61
+ end
62
+
63
+ DSLTest.set(:foo, NoAny.new)
64
+ DSLTest.any?(:foo).should eq('oh no')
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,104 @@
1
+ require 'spec_helper'
2
+ require 'centurion/deploy_dsl'
3
+ require 'capistrano_dsl'
4
+
5
+ class DeployDSLTest
6
+ extend Capistrano::DSL
7
+ extend Centurion::DeployDSL
8
+ end
9
+
10
+ describe Centurion::DeployDSL do
11
+ before do
12
+ DeployDSLTest.clear_env
13
+ DeployDSLTest.set_current_environment('test')
14
+ end
15
+
16
+ it 'exposes an easy wrapper for handling each Docker host' do
17
+ recipient = double('recipient')
18
+ expect(recipient).to receive(:ping).with('host1')
19
+ expect(recipient).to receive(:ping).with('host2')
20
+
21
+ DeployDSLTest.set(:hosts, %w{ host1 host2 })
22
+ DeployDSLTest.on_each_docker_host { |h| recipient.ping(h.hostname) }
23
+ end
24
+
25
+ it 'adds new env_vars to the existing ones' do
26
+ DeployDSLTest.set(:env_vars, { 'SHAKESPEARE' => 'Hamlet' })
27
+ DeployDSLTest.env_vars('DICKENS' => 'David Copperfield')
28
+
29
+ expect(DeployDSLTest.fetch(:env_vars)).to include(
30
+ 'SHAKESPEARE' => 'Hamlet',
31
+ 'DICKENS' => 'David Copperfield'
32
+ )
33
+ end
34
+
35
+ it 'adds hosts to the host list' do
36
+ DeployDSLTest.set(:hosts, [ 'host1' ])
37
+ DeployDSLTest.host('host2')
38
+
39
+ expect(DeployDSLTest).to have_key_and_value(:hosts, %w{ host1 host2 })
40
+ end
41
+
42
+ describe '#localhost' do
43
+ it 'adds a host by reading DOCKER_HOST if present' do
44
+ expect(ENV).to receive(:[]).with('DOCKER_HOST').and_return('tcp://127.1.1.1:4240')
45
+ DeployDSLTest.localhost
46
+ expect(DeployDSLTest).to have_key_and_value(:hosts, %w[ 127.1.1.1:4240 ])
47
+ end
48
+
49
+ it 'adds a host defaulting to loopback if DOCKER_HOST is not present' do
50
+ expect(ENV).to receive(:[]).with('DOCKER_HOST').and_return(nil)
51
+ DeployDSLTest.localhost
52
+ expect(DeployDSLTest).to have_key_and_value(:hosts, %w[ 127.0.0.1 ])
53
+ end
54
+ end
55
+
56
+ describe '#host_port' do
57
+ it 'raises unless passed container_port in the options' do
58
+ expect { DeployDSLTest.host_port(666, {}) }.to raise_error(ArgumentError, /:container_port/)
59
+ end
60
+
61
+ it 'adds new bind ports to the list' do
62
+ dummy_value = { '666/tcp' => ['value'] }
63
+ DeployDSLTest.set(:port_bindings, dummy_value)
64
+ DeployDSLTest.host_port(999, container_port: 80)
65
+
66
+ expect(DeployDSLTest).to have_key_and_value(
67
+ :port_bindings,
68
+ dummy_value.merge('80/tcp' => [{ 'HostIp' => '0.0.0.0', 'HostPort' => '999' }])
69
+ )
70
+ end
71
+
72
+ it 'does not explode if port_bindings is empty' do
73
+ expect { DeployDSLTest.host_port(999, container_port: 80) }.not_to raise_error
74
+ end
75
+
76
+ it 'raises if invalid options are passed' do
77
+ expect { DeployDSLTest.host_port(80, asdf: 'foo') }.to raise_error(ArgumentError, /invalid key!/)
78
+ end
79
+ end
80
+
81
+ describe '#host_volume' do
82
+ it 'raises unless passed the container_volume option' do
83
+ expect { DeployDSLTest.host_volume('foo', {}) }.to raise_error(ArgumentError, /:container_volume/)
84
+ end
85
+
86
+ it 'raises when passed bogus options' do
87
+ expect { DeployDSLTest.host_volume('foo', bogus: 1) }.to raise_error(ArgumentError, /invalid key!/)
88
+ end
89
+
90
+ it 'adds new host volumes' do
91
+ expect(DeployDSLTest.fetch(:binds)).to be_nil
92
+ DeployDSLTest.host_volume('volume1', container_volume: '/dev/sdd')
93
+ DeployDSLTest.host_volume('volume2', container_volume: '/dev/sde')
94
+ expect(DeployDSLTest.fetch(:binds)).to eq %w{ volume1:/dev/sdd volume2:/dev/sde }
95
+ end
96
+ end
97
+
98
+ it 'gets current tags for an image' do
99
+ Centurion::DockerServer.any_instance.stub(current_tags_for: [ 'foo' ])
100
+ DeployDSLTest.set(:hosts, [ 'host1' ])
101
+
102
+ expect(DeployDSLTest.get_current_tags_for('asdf')).to eq [ { server: 'host1', tags: [ 'foo'] } ]
103
+ end
104
+ end
@@ -0,0 +1,220 @@
1
+ require 'centurion/deploy'
2
+ require 'centurion/deploy_dsl'
3
+ require 'centurion/logging'
4
+
5
+ describe Centurion::Deploy do
6
+ let(:mock_ok_status) { double('http_status_ok').tap { |s| s.stub(status: 200) } }
7
+ let(:mock_bad_status) { double('http_status_ok').tap { |s| s.stub(status: 500) } }
8
+ let(:server) { double('docker_server').tap { |s| s.stub(hostname: 'host1'); s.stub(:attach) } }
9
+ let(:port) { 8484 }
10
+ let(:container) { { 'Ports' => [{ 'PublicPort' => port }, 'Created' => Time.now.to_i ], 'Id' => '21adfd2ef2ef2349494a', 'Names' => [ 'name1' ] } }
11
+ let(:endpoint) { '/status/check' }
12
+ let(:test_deploy) do
13
+ Object.new.tap do |o|
14
+ o.send(:extend, Centurion::Deploy)
15
+ o.send(:extend, Centurion::DeployDSL)
16
+ o.send(:extend, Centurion::Logging)
17
+ end
18
+ end
19
+
20
+ describe '#http_status_ok?' do
21
+ it 'validates HTTP status checks when the response is good' do
22
+ expect(Excon).to receive(:get).and_return(mock_ok_status)
23
+ expect(test_deploy.http_status_ok?(server, port, endpoint)).to be_true
24
+ end
25
+
26
+ it 'identifies bad HTTP responses' do
27
+ expect(Excon).to receive(:get).and_return(mock_bad_status)
28
+ test_deploy.stub(:warn)
29
+ expect(test_deploy.http_status_ok?(server, port, endpoint)).to be_false
30
+ end
31
+
32
+ it 'outputs the HTTP status when it is not OK' do
33
+ expect(Excon).to receive(:get).and_return(mock_bad_status)
34
+ expect(test_deploy).to receive(:warn).with(/Got HTTP status: 500/)
35
+ expect(test_deploy.http_status_ok?(server, port, endpoint)).to be_false
36
+ end
37
+
38
+ it 'handles SocketErrors and outputs a message' do
39
+ expect(Excon).to receive(:get).and_raise(Excon::Errors::SocketError.new(RuntimeError.new()))
40
+ expect(test_deploy).to receive(:warn).with(/Failed to connect/)
41
+ expect(test_deploy.http_status_ok?(server, port, endpoint)).to be_false
42
+ end
43
+ end
44
+
45
+ describe '#container_up?' do
46
+ it 'recognizes when no containers are running' do
47
+ expect(server).to receive(:find_containers_by_public_port).and_return([])
48
+
49
+ test_deploy.container_up?(server, port).should be_false
50
+ end
51
+
52
+ it 'complains when more than one container is bound to this port' do
53
+ expect(server).to receive(:find_containers_by_public_port).and_return([1,2])
54
+ expect(test_deploy).to receive(:error).with /More than one container/
55
+
56
+ test_deploy.container_up?(server, port).should be_false
57
+ end
58
+
59
+ it 'recognizes when the container is actually running' do
60
+ expect(server).to receive(:find_containers_by_public_port).and_return([container])
61
+ expect(test_deploy).to receive(:info).with /Found container/
62
+
63
+ test_deploy.container_up?(server, port).should be_true
64
+ end
65
+ end
66
+
67
+ describe '#wait_for_http_status_ok?' do
68
+ before do
69
+ test_deploy.stub(:info)
70
+ end
71
+
72
+ it 'identifies that a container is up' do
73
+ test_deploy.stub(:container_up? => true)
74
+ test_deploy.stub(:http_status_ok? => true)
75
+
76
+ test_deploy.wait_for_http_status_ok(server, port, '/foo', 'image_id', 'chaucer')
77
+ expect(test_deploy).to have_received(:info).with(/Waiting for the port/)
78
+ expect(test_deploy).to have_received(:info).with('Container is up!')
79
+ end
80
+
81
+ it 'waits when the container is not yet up' do
82
+ test_deploy.stub(:container_up? => false)
83
+ test_deploy.stub(:error)
84
+ test_deploy.stub(:warn)
85
+ expect(test_deploy).to receive(:exit)
86
+ expect(test_deploy).to receive(:sleep).with(0)
87
+
88
+ test_deploy.wait_for_http_status_ok(server, port, '/foo', 'image_id', 'chaucer', 0, 1)
89
+ expect(test_deploy).to have_received(:info).with(/Waiting for the port/)
90
+ end
91
+
92
+ it 'waits when the HTTP status is not OK' do
93
+ test_deploy.stub(:container_up? => true)
94
+ test_deploy.stub(:http_status_ok? => false)
95
+ test_deploy.stub(:error)
96
+ test_deploy.stub(:warn)
97
+ expect(test_deploy).to receive(:exit)
98
+
99
+ test_deploy.wait_for_http_status_ok(server, port, '/foo', 'image_id', 'chaucer', 1, 0)
100
+ expect(test_deploy).to have_received(:info).with(/Waiting for the port/)
101
+ end
102
+ end
103
+
104
+ describe '#cleanup_containers' do
105
+ it 'deletes all but two containers' do
106
+ expect(server).to receive(:old_containers_for_port).with(port.to_s).and_return([
107
+ {'Id' => '123', 'Names' => ['foo']},
108
+ {'Id' => '456', 'Names' => ['foo']},
109
+ {'Id' => '789', 'Names' => ['foo']},
110
+ {'Id' => '0ab', 'Names' => ['foo']},
111
+ {'Id' => 'cde', 'Names' => ['foo']},
112
+ ])
113
+ expect(server).to receive(:remove_container).with('789')
114
+ expect(server).to receive(:remove_container).with('0ab')
115
+ expect(server).to receive(:remove_container).with('cde')
116
+
117
+ test_deploy.cleanup_containers(server, {'80/tcp' => [{'HostIp' => '0.0.0.0', 'HostPort' => port.to_s}]})
118
+ end
119
+ end
120
+
121
+ describe '#stop_containers' do
122
+ it 'calls stop_container on the right containers' do
123
+ second_container = container.dup
124
+ containers = [ container, second_container ]
125
+ bindings = {'80/tcp'=>[{'HostIp'=>'0.0.0.0', 'HostPort'=>'80'}]}
126
+
127
+ expect(server).to receive(:find_containers_by_public_port).and_return(containers)
128
+ expect(test_deploy).to receive(:public_port_for).with(bindings).and_return('80')
129
+ expect(server).to receive(:stop_container).with(container['Id']).once
130
+ expect(server).to receive(:stop_container).with(second_container['Id']).once
131
+
132
+ test_deploy.stop_containers(server, bindings)
133
+ end
134
+ end
135
+
136
+ describe '#wait_for_load_balancer_check_interval' do
137
+ it 'knows how long to sleep' do
138
+ timing = double(timing)
139
+ expect(test_deploy).to receive(:fetch).with(:rolling_deploy_check_interval, 5).and_return(timing)
140
+ expect(test_deploy).to receive(:sleep).with(timing)
141
+
142
+ test_deploy.wait_for_load_balancer_check_interval
143
+ end
144
+ end
145
+
146
+ describe '#container_config_for' do
147
+ it 'works with env_vars provided' do
148
+ config = test_deploy.container_config_for(server, 'image_id', {}, 'FOO' => 'BAR')
149
+
150
+ expect(config).to be_a(Hash)
151
+ expect(config.keys).to match_array(%w{ Hostname Image Env ExposedPorts })
152
+ expect(config['Env']).to eq(['FOO=BAR'])
153
+ end
154
+
155
+ it 'works without env_vars or port_bindings' do
156
+ config = test_deploy.container_config_for(server, 'image_id')
157
+
158
+ expect(config).to be_a(Hash)
159
+ expect(config.keys).to match_array(%w{ Hostname Image })
160
+ end
161
+ end
162
+
163
+ describe '#start_new_container' do
164
+ let(:bindings) { {'80/tcp'=>[{'HostIp'=>'0.0.0.0', 'HostPort'=>'80'}]} }
165
+
166
+ it 'configures the container' do
167
+ expect(test_deploy).to receive(:container_config_for).with(server, 'image_id', bindings, nil).once
168
+ test_deploy.stub(:start_container_with_config)
169
+
170
+ test_deploy.start_new_container(server, 'image_id', bindings, {})
171
+ end
172
+
173
+ it 'starts the container' do
174
+ expect(test_deploy).to receive(:start_container_with_config).with(server, {}, anything(), anything())
175
+
176
+ test_deploy.start_new_container(server, 'image_id', bindings, {})
177
+ end
178
+
179
+ it 'ultimately asks the server object to do the work' do
180
+ server.should_receive(:create_container).with(
181
+ hash_including({'Image'=>'image_id', 'Hostname'=>'host1', 'ExposedPorts'=>{'80/tcp'=>{}}})
182
+ ).and_return(container)
183
+
184
+ server.should_receive(:start_container)
185
+ server.should_receive(:inspect_container)
186
+
187
+ new_container = test_deploy.start_new_container(server, 'image_id', bindings, {})
188
+ expect(new_container).to eq(container)
189
+ end
190
+ end
191
+
192
+ describe '#launch_console' do
193
+ let(:bindings) { {'80/tcp'=>[{'HostIp'=>'0.0.0.0', 'HostPort'=>'80'}]} }
194
+
195
+ it 'configures the container' do
196
+ expect(test_deploy).to receive(:container_config_for).with(server, 'image_id', bindings, nil).once
197
+ test_deploy.stub(:start_container_with_config)
198
+
199
+ test_deploy.start_new_container(server, 'image_id', bindings, {})
200
+ end
201
+
202
+ it 'augments the container_config' do
203
+ expect(test_deploy).to receive(:start_container_with_config).with(server, {},
204
+ anything(),
205
+ hash_including('Cmd' => [ '/bin/bash' ], 'AttachStdin' => true , 'Tty' => true , 'OpenStdin' => true)
206
+ ).and_return({'Id' => 'shakespeare'})
207
+
208
+ test_deploy.launch_console(server, 'image_id', bindings, {})
209
+ end
210
+
211
+ it 'starts the console' do
212
+ expect(test_deploy).to receive(:start_container_with_config).with(
213
+ server, {}, anything(), anything()
214
+ ).and_return({'Id' => 'shakespeare'})
215
+
216
+ test_deploy.launch_console(server, 'image_id', bindings, {})
217
+ expect(server).to have_received(:attach).with('shakespeare')
218
+ end
219
+ end
220
+ end
@@ -0,0 +1,31 @@
1
+ require 'spec_helper'
2
+ require 'centurion/docker_server'
3
+ require 'centurion/docker_server_group'
4
+
5
+ describe Centurion::DockerServerGroup do
6
+ let(:docker_path) { 'docker' }
7
+ let(:group) { Centurion::DockerServerGroup.new(['host1', 'host2'], docker_path) }
8
+
9
+ it 'takes a hostlist and instantiates DockerServers' do
10
+ expect(group.hosts).to have(2).items
11
+ expect(group.hosts.first).to be_a(Centurion::DockerServer)
12
+ expect(group.hosts.last).to be_a(Centurion::DockerServer)
13
+ end
14
+
15
+ it 'implements Enumerable' do
16
+ expect(group.methods).to be_a_kind_of(Enumerable)
17
+ end
18
+
19
+ it 'prints a friendly message to stderr when iterating' do
20
+ expect(group).to receive(:info).with(/Connecting to Docker on host[0-9]/).twice
21
+
22
+ group.each { |host| }
23
+ end
24
+
25
+ it 'can run parallel operations' do
26
+ item = double('item').tap { |i| i.stub(:dummy_method) }
27
+ expect(item).to receive(:dummy_method).twice
28
+
29
+ expect { group.each_in_parallel { |host| item.dummy_method } }.not_to raise_error
30
+ end
31
+ end