auto-consul 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.
@@ -0,0 +1,100 @@
1
+ require 'spec-helper'
2
+
3
+ describe AutoConsul::Cluster do
4
+ let(:uri) { "s3://some-bucket-#{double.object_id}/some/prefix/#{double.object_id}" }
5
+ let(:servers_uri) { "#{uri}/servers" }
6
+ let(:agents_uri) { "#{uri}/agents" }
7
+ let(:registry_lookup) { AutoConsul::Cluster.should_receive(:get_provider_for_uri) }
8
+
9
+ subject { AutoConsul::Cluster.new uri }
10
+
11
+ it 'should get provider for */servers' do
12
+ registry_lookup.once.with(servers_uri).and_return(provider = double("S3Provider"))
13
+ expect(subject.servers).to equal(provider)
14
+ expect(subject.servers).to equal(provider)
15
+ end
16
+
17
+ it 'should get provider for */agents' do
18
+ registry_lookup.once.with(agents_uri).and_return(provider = double("S3Provider"))
19
+ subject.agents
20
+ expect(subject.agents).to equal(provider)
21
+ expect(subject.agents).to equal(provider)
22
+ end
23
+
24
+ describe 'set_mode!' do
25
+ let(:agents) { [] }
26
+ let(:servers) { [] }
27
+ let(:expiry) { double }
28
+ let(:local_state) { double }
29
+
30
+ before do
31
+ subject.stub(:agents).and_return(agents_reg = double)
32
+ agents_reg.stub(:members).with(expiry).and_return agents
33
+ subject.stub(:servers).and_return(servers_reg = double)
34
+ servers_reg.stub(:members).with(expiry).and_return servers
35
+ end
36
+
37
+ describe 'with no active servers' do
38
+ before do
39
+ # Default desired servers of 1.
40
+ local_state.should_receive(:set_server!)
41
+ end
42
+
43
+ it 'should set_server! with no server count specified' do
44
+ subject.set_mode! local_state, expiry
45
+ end
46
+
47
+ it 'should set_server! with a server count specified' do
48
+ subject.set_mode! local_state, expiry, 2
49
+ end
50
+ end
51
+
52
+ describe 'without enough active servers' do
53
+ before do
54
+ local_state.should_receive(:set_server!).with
55
+ local_state.should_not_receive(:set_agent!)
56
+ end
57
+
58
+ it 'should set_server! given 1 server but wanting 3' do
59
+ servers << double
60
+ subject.set_mode! local_state, expiry, 3
61
+ end
62
+
63
+ it 'should set_server! given 3 servers but wanting 4' do
64
+ servers << double
65
+ servers << double
66
+ servers << double
67
+ subject.set_mode! local_state, expiry, 4
68
+ end
69
+ end
70
+
71
+ describe 'with enough active servers' do
72
+ before do
73
+ servers << double
74
+ local_state.should_receive(:set_agent!).with
75
+ local_state.should_not_receive(:set_server!)
76
+ end
77
+
78
+ it 'should set_agent! given 1 server, wanting default' do
79
+ subject.set_mode! local_state, expiry
80
+ end
81
+
82
+ it 'should set_agent! given 1 server, wanting 1' do
83
+ subject.set_mode! local_state, expiry, 1
84
+ end
85
+
86
+ it 'should set_agent! given 3 servers, wanting 3' do
87
+ servers << double
88
+ servers << double
89
+ subject.set_mode! local_state, expiry, 3
90
+ end
91
+
92
+ it 'should set_agent! given 3 servers, wanting 2' do
93
+ servers << double
94
+ servers << double
95
+ subject.set_mode! local_state, expiry, 2
96
+ end
97
+ end
98
+ end
99
+ end
100
+
@@ -0,0 +1,110 @@
1
+ require 'spec-helper'
2
+
3
+ shared_examples_for 'a server' do
4
+ it 'should use "PATH/server" for data path' do
5
+ subject.data_path.should == File.join(tempdir.path 'server')
6
+ end
7
+
8
+ it 'should be true for server?' do
9
+ subject.should be_server
10
+ end
11
+
12
+ it 'should be false for agent?' do
13
+ subject.should_not be_agent
14
+ end
15
+ end
16
+
17
+ shared_examples_for 'an agent' do
18
+ it 'should use "PATH/agent" for data path' do
19
+ subject.data_path.should == File.join(tempdir.path 'agent')
20
+ end
21
+
22
+ it 'should be false for server?' do
23
+ subject.should_not be_server
24
+ end
25
+
26
+ it 'should be true for agent?' do
27
+ subject.should be_agent
28
+ end
29
+ end
30
+
31
+ tempdir_context AutoConsul::Local do
32
+ subject { AutoConsul::Local.bind_to_path path }
33
+
34
+ let(:path) { tempdir.path }
35
+
36
+ context 'given a non-existent path' do
37
+ let(:path) { tempdir.path 'faux', 'state', 'directory' }
38
+
39
+ it 'should create the desired path' do
40
+ File.directory?(subject.path).should be_true
41
+ end
42
+ end
43
+
44
+ it 'should use "PATH/mode" for mode_path' do
45
+ subject.mode_path.should == File.join(path, 'mode')
46
+ end
47
+
48
+ context 'set to server mode' do
49
+ before do
50
+ subject.set_server!
51
+ end
52
+
53
+ it 'should enter "server" in mode file' do
54
+ File.open(tempdir.path('mode'), 'r').read.should == 'server'
55
+ end
56
+
57
+ it_should_behave_like 'a server'
58
+ end
59
+
60
+ context 'set to agent mode' do
61
+ before do
62
+ subject.set_agent!
63
+ end
64
+
65
+ it 'should enter "agent" in mode file' do
66
+ File.open(tempdir.path('mode'), 'r').read.should == 'agent'
67
+ end
68
+
69
+ it_should_behave_like 'an agent'
70
+ end
71
+
72
+ context 'given a server mode file' do
73
+ before do
74
+ File.open(File.join(path, 'mode'), 'w') {|f| f.write 'server'}
75
+ end
76
+
77
+ it_should_behave_like 'a server'
78
+ end
79
+
80
+ context 'given an agent mode file' do
81
+ before do
82
+ File.open(File.join(path, 'mode'), 'w') {|f| f.write 'agent'}
83
+ end
84
+
85
+ it_should_behave_like 'an agent'
86
+ end
87
+
88
+ context 'given a bogus mode file' do
89
+ before do
90
+ File.open(File.join(path, 'mode'), 'w') {|f| f.write 'shmerver'}
91
+ end
92
+
93
+ it 'should have a nil mode' do
94
+ subject.mode.should be_nil
95
+ end
96
+
97
+ it 'should be false for server?' do
98
+ subject.should_not be_server
99
+ end
100
+
101
+ it 'should be false for agent?' do
102
+ subject.should_not be_agent
103
+ end
104
+
105
+ it 'should have a nil data_path' do
106
+ subject.data_path.should be_nil
107
+ end
108
+ end
109
+ end
110
+
@@ -0,0 +1,82 @@
1
+ require 'spec-helper'
2
+
3
+ def mock_output name
4
+ File.open(File.join(File.dirname(__FILE__), name), 'r') do |f|
5
+ f.read
6
+ end
7
+ end
8
+
9
+ shared_examples_for 'a running cli agent' do
10
+ it 'should be running' do
11
+ expect(subject).to be_running
12
+ end
13
+
14
+ it 'should be a running cli agent' do
15
+ expect(subject).to be_agent
16
+ end
17
+ end
18
+
19
+ describe AutoConsul::RunState::CLIProvider do
20
+ subject do
21
+ AutoConsul::RunState::CLIProvider.new
22
+ end
23
+
24
+ let :consul_fail do
25
+ c = subject.should_receive(:system).with do |cmd, opts|
26
+ cmd.should == 'consul info'
27
+ end
28
+ c.and_return false
29
+ c
30
+ end
31
+
32
+ let :consul_call do
33
+ subject.should_receive(:system) do |cmd, opts|
34
+ cmd.should == 'consul info'
35
+ opts[:out].write(output)
36
+ end
37
+ end
38
+
39
+ describe 'with no running consul' do
40
+ before do
41
+ consul_fail
42
+ end
43
+
44
+ it 'should not be running' do
45
+ expect(subject).not_to be_running
46
+ end
47
+
48
+ it 'should not be a running cli agent' do
49
+ expect(subject).not_to be_agent
50
+ end
51
+
52
+ it 'should not be a server' do
53
+ expect(subject).not_to be_server
54
+ end
55
+ end
56
+
57
+ describe 'with running consul' do
58
+ before do
59
+ consul_call
60
+ end
61
+
62
+ describe 'as normal agent' do
63
+ let(:output) { mock_output 'agent-output.txt' }
64
+
65
+ it_should_behave_like 'a running cli agent'
66
+
67
+ it 'should not be a server' do
68
+ expect(subject).not_to be_server
69
+ end
70
+ end
71
+
72
+ describe 'as server' do
73
+ let(:output) { mock_output 'server-output.txt' }
74
+
75
+ it_should_behave_like 'a running cli agent'
76
+
77
+ it 'should be a server' do
78
+ expect(subject).to be_server
79
+ end
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,75 @@
1
+ require 'spec-helper'
2
+
3
+ shared_examples_for 'a consul agent run' do |method_name, registry_name, join_flag, args|
4
+ it 'properly launches consul agent' do
5
+ members = []
6
+ members << member if join_flag
7
+
8
+ registry.should_receive(registry_name).with.and_return(reg = double)
9
+ reg.should_receive(:members).with(expiry).and_return(members)
10
+
11
+ expected_args = (['consul', 'agent', '-bind', ip, '-data-dir', data_dir, '-node', identity] + args).collect do |e|
12
+ if e.instance_of? Symbol
13
+ send e
14
+ else
15
+ e
16
+ end
17
+ end
18
+
19
+ AutoConsul::Runner.should_receive(:spawn).with(*expected_args).and_return(agent_pid = double)
20
+
21
+ # consul info retries to verify that it's running.
22
+ AutoConsul::Runner.should_receive(:system).with('consul', 'info').and_return(false, false, true)
23
+
24
+ AutoConsul::Runner.should_receive(:sleep).with(2)
25
+ AutoConsul::Runner.should_receive(:sleep).with(4)
26
+ AutoConsul::Runner.should_receive(:sleep).with(6)
27
+
28
+ if join_flag
29
+ AutoConsul::Runner.should_receive(:system).with('consul', 'join', remote_ip).and_return(true)
30
+ end
31
+
32
+ Process.should_receive(:wait).with(agent_pid)
33
+
34
+ callable = AutoConsul::Runner.method(method_name)
35
+ callable.call(identity, ip, expiry, local_state, registry)
36
+ end
37
+ end
38
+
39
+ describe AutoConsul::Runner do
40
+ let(:ip) { "192.168.50.101" }
41
+ let(:remote_ip) { "192.168.50.102" }
42
+ let(:member) { double("ClusterMember", :identity => 'foo', :time => double, :data => remote_ip) }
43
+ let(:agents_list) { [] }
44
+ let(:servers_list) { [] }
45
+ let(:identity) { "id-#{double.object_id}" }
46
+ let(:data_dir) { "/var/lib/consul/#{double.object_id}" }
47
+ let(:local_state) { double("FileSystemState", :data_path => data_dir) }
48
+ let(:registry) { double("Registry", :agents => double("S3Provider"),
49
+ :servers => double("S3Provider")) }
50
+
51
+ let(:expiry) { 120.to_i }
52
+
53
+ before do
54
+ registry.agents.stub(:agents).with(expiry).and_return(agents_list)
55
+ registry.servers.stub(:servers).with(expiry).and_return(servers_list)
56
+ end
57
+
58
+ describe :run_agent! do
59
+ it_behaves_like 'a consul agent run', :run_agent!, :agents, true, []
60
+ end
61
+
62
+ describe :run_server! do
63
+ describe 'with empty server registry' do
64
+ # consul agent -bind 192.168.50.100 -data-dir /opt/consul/server/data -node vagrant-server -server -bootstrap
65
+ it_behaves_like 'a consul agent run', :run_server!, :servers, false, ['-server', '-bootstrap']
66
+ end
67
+
68
+ describe 'with other servers in registry' do
69
+ # consul agent -bind 192.168.50.100 -data-dir /opt/consul/server/data -node vagrant-server -server
70
+ # consul join some_ip
71
+
72
+ it_behaves_like 'a consul agent run', :run_server!, :servers, true, ['-server']
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,239 @@
1
+ require 'spec-helper'
2
+
3
+ shared_examples_for 'a heartbeat emitter' do
4
+ it 'should write a TIMESTAMP-IDENTIFIER entry under the key prefix with the payload as data.' do
5
+ objects.should_receive(:[]).with(File.join(path, "#{stamp}-#{identity}")).and_return(obj = double)
6
+ obj.should_receive(:write).with(payload = "Some payload data #{double.to_s}")
7
+ subject.heartbeat! identity, payload, expiry
8
+ end
9
+ end
10
+
11
+ shared_examples_for 'has valid members' do |locator_pairs|
12
+ let :member_pairs do
13
+ locator_pairs.collect do |time_sym, identifier|
14
+ [send(time_sym), identifier]
15
+ end
16
+ end
17
+
18
+ it 'gets appropriate identifier and timestamp per member' do
19
+ m = subject.members(expiry).collect {|mem| [mem.time, mem.identifier]}
20
+ m.should == member_pairs
21
+ end
22
+
23
+ it 'does not read object data if not requested' do
24
+ member_pairs.each do |pair|
25
+ s3_cache[pair].should_not_receive(:read)
26
+ end
27
+
28
+ subject.members(expiry).each {|mem| :amazing}
29
+ end
30
+
31
+ it 'exposes object data through the :data method' do
32
+ m = subject.members(expiry).collect {|mem| mem.data}
33
+ m.should == member_pairs.collect {|p| s3_cache[p].read}
34
+ end
35
+ end
36
+
37
+ describe AutoConsul::Cluster::Registry::S3Provider do
38
+ let(:provider) { AutoConsul::Cluster::Registry::S3Provider }
39
+ let(:bucket) { "bucket-#{self.object_id.to_s}" }
40
+ let(:path) { "#{self.object_id.to_s}/foo/bar" }
41
+ let(:uri) { "s3://#{bucket}/#{path}" }
42
+
43
+ context 'retrieved via AutoConsul::Cluster.get_provider_for_uri' do
44
+ it 'should be the provider for an s3:// URL' do
45
+ AutoConsul::Cluster.get_provider_for_uri(uri).should be_a provider
46
+ end
47
+
48
+ it 'should be instantiated with the uri string as URI instance' do
49
+ provider.should_receive(:new).with(URI(uri))
50
+ AutoConsul::Cluster.get_provider_for_uri(uri)
51
+ end
52
+ end
53
+
54
+ describe 'given a URI instance' do
55
+ subject { provider.new URI(uri) }
56
+
57
+ it 'should get the bucket name from the URI host' do
58
+ subject.bucket_name.should == bucket
59
+ end
60
+
61
+ it 'should get the key prefix from the URI path' do
62
+ subject.key_prefix.should == path
63
+ end
64
+
65
+ it 'should get the bucket object using the bucket name and the AWS SDK' do
66
+ AWS::S3.stub(:new).and_return(s3 = double)
67
+ s3.should_receive(:buckets).with.and_return(buckets = double)
68
+ buckets.should_receive(:[]).with(bucket).and_return(bucket_object = double)
69
+ subject.bucket.should == bucket_object
70
+ end
71
+
72
+ context 'with a bucket' do
73
+ let(:bucket_object) { double }
74
+ let(:objects) { double }
75
+ let(:time) do
76
+ t = Time.now.utc
77
+ Time.utc t.year, t.month, t.day, t.hour, t.min, t.sec, 0
78
+ end
79
+ let(:expiry) { 120.to_i }
80
+ let(:check_time) { time - expiry }
81
+
82
+ before do
83
+ subject.stub(:bucket).with.and_return(bucket_object)
84
+ bucket_object.stub(:objects).with.and_return(objects)
85
+ end
86
+
87
+ describe 'the heartbeat method' do
88
+ let(:identity) { "#{self.object_id.to_s}-identifier" }
89
+ let(:stamp) { time.dup.utc.strftime('%Y%m%d%H%M%S') }
90
+
91
+ before do
92
+ t = time
93
+ Time.stub(:now).with.and_return(t)
94
+ end
95
+
96
+ describe 'with no expiry' do
97
+ let(:expiry) { nil }
98
+
99
+ before do
100
+ objects.should_not_receive(:with_prefix)
101
+ bucket.should_not_receive(:delete_if)
102
+ objects.should_not_receive(:delete_if)
103
+ bucket.should_not_receive(:delete)
104
+ objects.should_not_receive(:delete)
105
+ end
106
+
107
+ it_should_behave_like 'a heartbeat emitter'
108
+ end
109
+
110
+ describe 'with an expiry' do
111
+ let(:expiry) { 145.to_i }
112
+ let(:check_time) { time.dup.utc - expiry }
113
+
114
+ let :pre_expiration do
115
+ double "S3Object", :key => File.join(path, "#{(check_time - 1).strftime('%Y%m%d%H%M%S')}-foo")
116
+ end
117
+
118
+ let :on_expiration do
119
+ double "S3Object", :key => File.join(path, "#{check_time.strftime('%Y%m%d%H%M%S')}-bar")
120
+ end
121
+
122
+ let :post_expiration do
123
+ double "S3Object", :key => File.join(path, "#{(check_time + 1).strftime('%Y%m%d%H%M%S')}-baz")
124
+ end
125
+
126
+ before do
127
+ objects.should_receive(:with_prefix).with(path).and_return(with_pre = double)
128
+ with_pre.should_receive(:delete_if) do |&block|
129
+ block.call(pre_expiration).should be_true
130
+ block.call(post_expiration).should be_false
131
+ block.call(on_expiration).should be_true
132
+ end
133
+ end
134
+
135
+ it_should_behave_like 'a heartbeat emitter'
136
+ end
137
+ end
138
+
139
+ describe 'and no heartbeats' do
140
+ before do
141
+ objects.should_receive(:with_prefix).with(path).and_return(collection = double)
142
+ # Doesn't yield, and thus is "empty".
143
+ collection.should_receive(:each).and_return(collection)
144
+ end
145
+ end
146
+
147
+ describe 'and heartbeats' do
148
+ let(:s3_cache) { {} }
149
+ let :deletes do
150
+ key_source.find_all {|pair| pair[0] <= check_time}.collect do |pair|
151
+ s3_cache[pair]
152
+ end
153
+ end
154
+
155
+ before do
156
+ with_pre = objects.should_receive(:with_prefix).with(path).and_return(with_prefix_each = double)
157
+ with_prefix_each = with_prefix_each.should_receive(:each).with
158
+ key_source.inject(with_prefix_each) do |o, pair|
159
+ s3_cache[pair] = double("S3Object",
160
+ :key => File.join(path, "#{pair[0].strftime('%Y%m%d%H%M%S')}-#{pair[1]}"),
161
+ :read => pair[1])
162
+ o.and_yield(s3_cache[pair])
163
+ end
164
+
165
+ with_prefix_each.and_return(with_prefix_each) if key_source.size < 1
166
+ end
167
+
168
+ before do
169
+ if deletes.size > 0
170
+ objects.should_receive(:delete).with(deletes)
171
+ else
172
+ objects.should_not_receive(:delete)
173
+ end
174
+ end
175
+
176
+ describe 'past expiration' do
177
+ let(:earliest) { (check_time - 10).utc }
178
+ let(:early) { (check_time - 5).utc }
179
+ let(:max_time) { check_time.dup.utc }
180
+
181
+ let :key_source do
182
+ [[earliest, 'earliest'],
183
+ [early, 'early'],
184
+ [max_time, 'expiry']]
185
+ end
186
+
187
+ it 'should have an empty members list' do
188
+ subject.members(expiry).should == []
189
+ end
190
+ end
191
+
192
+ describe 'that are live' do
193
+ let(:valid_early) { (check_time + 1).utc }
194
+ let(:valid_late) { (time - 1).utc }
195
+
196
+ describe 'only' do
197
+ let :key_source do
198
+ [[valid_early, 'both'],
199
+ [valid_early, 'early_only'],
200
+ [valid_late, 'both'],
201
+ [valid_late, 'late_only']]
202
+ end
203
+
204
+ it_should_behave_like 'has valid members', [
205
+ [:valid_early, 'early_only'],
206
+ [:valid_late, 'both'],
207
+ [:valid_late, 'late_only']]
208
+ end
209
+
210
+ describe 'mixed with expired heartbeats' do
211
+ let(:expired_early) { (check_time - 10).utc }
212
+ let(:expired_late) { check_time.dup.utc }
213
+
214
+ let :key_source do
215
+ [[expired_early, 'expired_and_valid'],
216
+ [expired_early, 'expired_early'],
217
+ [expired_late, 'expired_late'],
218
+ [expired_late, 'check_time_and_valid'],
219
+ [valid_early, 'both'],
220
+ [valid_early, 'early_only'],
221
+ [valid_early, 'expired_and_valid'],
222
+ [valid_late, 'both'],
223
+ [valid_late, 'check_time_and_valid'],
224
+ [valid_late, 'late_only']]
225
+ end
226
+
227
+ it_should_behave_like 'has valid members', [
228
+ [:valid_early, 'early_only'],
229
+ [:valid_early, 'expired_and_valid'],
230
+ [:valid_late, 'both'],
231
+ [:valid_late, 'check_time_and_valid'],
232
+ [:valid_late, 'late_only']]
233
+ end
234
+ end
235
+ end
236
+ end
237
+ end
238
+ end
239
+
@@ -0,0 +1,43 @@
1
+ require 'rspec'
2
+ require 'fileutils'
3
+ require 'tempfile'
4
+ require 'auto-consul'
5
+
6
+ module AutoConsulTest
7
+ # A wrapper for a temporary directory.
8
+ #
9
+ # While initialization will create a temporary directory, it is the
10
+ # caller's responsibility to clean things up.
11
+ class Tempdir
12
+ def initialize
13
+ @path = File.expand_path(Dir.mktmpdir)
14
+ end
15
+
16
+ # Helper that joins `paths` to the temporary directory for easy
17
+ # temporary-directory-internal path generation.
18
+ #
19
+ # Call it with no arguments to get the temporary directory itself.
20
+ def path(*paths)
21
+ if paths.size > 0
22
+ File.join @path, *paths
23
+ else
24
+ @path
25
+ end
26
+ end
27
+ end
28
+ end
29
+
30
+ module RSpec::Core::DSL
31
+ def tempdir_context(*args, &block)
32
+ describe(*args) do
33
+ let(:tempdir) { AutoConsulTest::Tempdir.new }
34
+
35
+ after do
36
+ ::FileUtils.remove_entry tempdir.path
37
+ end
38
+
39
+ instance_eval(&block)
40
+ end
41
+ end
42
+ end
43
+