etcd 0.0.6 → 0.2.0.alpha

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,10 @@
1
+ # Encoding: utf-8
2
+
3
+ require 'mixlib/log'
4
+ module Etcd
5
+ ##
6
+ # A wrapper class that extends Mixlib::Log
7
+ class Log
8
+ extend Mixlib::Log
9
+ end
10
+ end
@@ -0,0 +1,28 @@
1
+ # Encoding: utf-8
2
+
3
+ require 'timeout'
4
+
5
+ module Etcd
6
+ module Mod
7
+ module Leader
8
+
9
+ def mod_leader_endpoint
10
+ '/mod/v2/leader'
11
+ end
12
+
13
+ def set_leader(key, value, ttl)
14
+ path = mod_leader_endpoint + "#{key}?ttl=#{ttl}"
15
+ api_execute(path, :put, params:{name: value}).body
16
+ end
17
+
18
+ def get_leader(key)
19
+ api_execute(mod_leader_endpoint + key, :get).body
20
+ end
21
+
22
+ def delete_leader(key, value)
23
+ path = mod_leader_endpoint + key + '?' + URI.encode_www_form(name: value)
24
+ api_execute(path, :delete).body
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,59 @@
1
+ # Encoding: utf-8
2
+
3
+ require 'timeout'
4
+
5
+ module Etcd
6
+ module Mod
7
+ module Lock
8
+
9
+ def mod_lock_endpoint
10
+ '/mod/v2/lock'
11
+ end
12
+
13
+ def acquire_lock(key, ttl, opts={})
14
+ path = mod_lock_endpoint + key + "?ttl=#{ttl}"
15
+ timeout = opts[:timeout] || 60
16
+ Timeout::timeout(timeout) do
17
+ api_execute(path, :post, params:opts)
18
+ end
19
+ end
20
+
21
+ def renew_lock(key, ttl, opts={})
22
+ unless opts.has_key?(:index) or opts.has_key?(:value)
23
+ raise ArgumentError, 'You mast pass index or value'
24
+ end
25
+ path = mod_lock_endpoint + key + "?ttl=#{ttl}"
26
+ timeout = opts[:timeout] || 60
27
+ Timeout::timeout(timeout) do
28
+ api_execute(path, :put, params:opts).body
29
+ end
30
+ end
31
+
32
+ def get_lock(key, opts={})
33
+ api_execute(mod_lock_endpoint + key, :get, params:opts).body
34
+ end
35
+
36
+ def delete_lock(key, opts={})
37
+ unless opts.has_key?(:index) or opts.has_key?(:value)
38
+ raise ArgumentError, 'You must pass index or value'
39
+ end
40
+ api_execute(mod_lock_endpoint + key, :delete, params:opts)
41
+ end
42
+
43
+ def lock(key, ttl, opts={})
44
+ acquire_lock('/'+key, ttl, opts)
45
+ index= get_lock('/'+key, field: index)
46
+ begin
47
+ yield key
48
+ rescue Exception => e
49
+ raise e
50
+ ensure
51
+ delete_lock(key, index: index)
52
+ end
53
+ end
54
+
55
+ alias_method :retrive_lock, :get_lock
56
+ alias_method :release_lock, :delete_lock
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,44 @@
1
+ # Encoding: utf-8
2
+
3
+ module Etcd
4
+ class Node
5
+
6
+ include Comparable
7
+
8
+ attr_reader :created_index, :modified_index, :expiration, :ttl, :key, :value
9
+ alias :createdIndex :created_index
10
+ alias :modifiedIndex :modified_index
11
+
12
+ def initialize(opts={})
13
+ @created_index = opts['createdIndex']
14
+ @modified_index = opts['modifiedIndex']
15
+ @ttl = opts['ttl']
16
+ @key = opts['key']
17
+ @value = opts['value']
18
+ @expiration = opts['expiration']
19
+ @dir = opts['dir']
20
+
21
+ if opts['dir'] and (!!opts['nodes'])
22
+ opts['nodes'].each do |data|
23
+ children << Node.new(data)
24
+ end
25
+ end
26
+ end
27
+
28
+ def <=>(other)
29
+ key <=> other.key
30
+ end
31
+
32
+ def children
33
+ if directory?
34
+ @children ||= []
35
+ else
36
+ raise "This is not a directory, cant have children"
37
+ end
38
+ end
39
+
40
+ def directory?
41
+ !! @dir
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,33 @@
1
+ # Encoding: utf-8
2
+
3
+ require 'etcd/node'
4
+ require 'json'
5
+
6
+ module Etcd
7
+
8
+ class Response
9
+
10
+ extend Forwardable
11
+
12
+ attr_reader :action, :node, :etcd_index, :raft_index, :raft_term
13
+
14
+ def_delegators :@node, :key, :value, :directory?, :children
15
+
16
+ def initialize(opts, headers={})
17
+ @action = opts['action']
18
+ @node = Node.new(opts['node'])
19
+ @etcd_index = headers[:etcd_index]
20
+ @raft_index = headers[:raft_index]
21
+ @raft_term = headers[:raft_term]
22
+ end
23
+
24
+ def self.from_http_response(response)
25
+ data = JSON.parse(response.body)
26
+ headers = Hash.new
27
+ headers[:etcd_index] = response['X-Etcd-Index'].to_i
28
+ headers[:raft_index] = response['X-Raft-Index'].to_i
29
+ headers[:raft_term] = response['X-Raft-Term'].to_i
30
+ response = Response.new(data, headers)
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,25 @@
1
+ # Encoding: utf-8
2
+
3
+ require 'json'
4
+
5
+ module Etcd
6
+ module Stats
7
+
8
+ def stats_endpoint
9
+ version_prefix + '/stats'
10
+ end
11
+
12
+ def stats(type)
13
+ case type
14
+ when :leader
15
+ JSON.parse(api_execute(stats_endpoint+'/leader', :get).body)
16
+ when :store
17
+ JSON.parse(api_execute(stats_endpoint+'/store', :get).body)
18
+ when :self
19
+ JSON.parse(api_execute(stats_endpoint+'/self', :get).body)
20
+ else
21
+ raise ArgumentError, "Invalid stats type '#{type}'"
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,5 @@
1
+ # Encoding: utf-8
2
+
3
+ module Etcd
4
+ VERSION = "0.2.0.alpha"
5
+ end
@@ -0,0 +1,54 @@
1
+ require 'spec_helper'
2
+
3
+ describe Etcd::Client do
4
+
5
+ let(:client) do
6
+ Etcd.client
7
+ end
8
+
9
+ it 'should return the leader address' do
10
+ expect(client.leader).to_not be_nil
11
+ end
12
+
13
+ it '#machines' do
14
+ expect(client.machines).to include('http://127.0.0.1:4001')
15
+ end
16
+
17
+ it '#version' do
18
+ expect(client.version).to match(/^etcd v0\.2\./)
19
+ end
20
+
21
+ it '#version_prefix' do
22
+ expect(client.version_prefix).to eq('/v2')
23
+ end
24
+
25
+ context '#api_execute' do
26
+ it 'should raise exception when non http methods are passed' do
27
+ expect do
28
+ client.api_execute('/v2/keys/x', :do)
29
+ end.to raise_error
30
+ end
31
+
32
+ it 'should redirect api request when allo_redirect is set'
33
+ end
34
+
35
+ context '#http header based metadata' do
36
+ before(:all) do
37
+ key = random_key
38
+ value = uuid.generate
39
+ @response = Etcd.client.set(key,value)
40
+ end
41
+
42
+ it '#etcd_index' do
43
+ expect(@response.etcd_index).to_not be_nil
44
+ end
45
+
46
+ it '#raft_index' do
47
+ expect(@response.raft_index).to_not be_nil
48
+ end
49
+
50
+ it '#raft_term' do
51
+ expect(@response.raft_term).to_not be_nil
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,26 @@
1
+ require 'spec_helper'
2
+
3
+ describe Etcd::Keys do
4
+
5
+ let(:client) do
6
+ Etcd.client
7
+ end
8
+
9
+ it '#set/#get' do
10
+ key = random_key
11
+ value = uuid.generate
12
+ client.set(key, value)
13
+ expect(client.get(key).value).to eq(value)
14
+ end
15
+
16
+ context '#exists?' do
17
+ it 'should be true for existing keys' do
18
+ key = random_key
19
+ client.create(key, 10)
20
+ expect(client.exists?(key)).to be_true
21
+ end
22
+ it 'should be true for existing keys' do
23
+ expect(client.exists?(random_key)).to be_false
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,29 @@
1
+ # Encoding: utf-8
2
+
3
+ require 'spec_helper'
4
+
5
+ describe 'mod leader' do
6
+
7
+ let(:client) do
8
+ Etcd.client
9
+ end
10
+
11
+ it 'should allow setting a key value with ttl' do
12
+ client.set_leader('/db_master1', 'db01', 10)
13
+ expect(client.get_leader('/db_master1')).to eq('db01')
14
+ end
15
+
16
+ it 'should allow deleting key with value' do
17
+ client.set_leader('/db_master4', 'db04', 10)
18
+ expect do
19
+ client.delete_leader('/db_master4', 'db04')
20
+ end.to_not raise_error
21
+ end
22
+
23
+ it 'should not allow deleting key without value' do
24
+ client.set_leader('/db_master5', 'db05', 10)
25
+ expect do
26
+ client.delete_leader('/db_master5', 'db04')
27
+ end.to raise_error
28
+ end
29
+ end
@@ -0,0 +1,46 @@
1
+ # Encoding: utf-8
2
+
3
+ require 'spec_helper'
4
+
5
+ describe 'lock' do
6
+
7
+ let(:client) do
8
+ Etcd.client
9
+ end
10
+
11
+ it 'should be able to acquire a lock' do
12
+ expect do
13
+ client.acquire_lock('/my_lock',10)
14
+ end.to_not raise_error
15
+ end
16
+
17
+ it 'should be able to renew a lock based on value' do
18
+ client.acquire_lock('/my_lock1', 10, value: 123)
19
+ expect do
20
+ client.renew_lock('/my_lock1', 10, value: 123)
21
+ end.to_not raise_error
22
+ end
23
+
24
+ it 'should be able to renew a lock based on index' do
25
+ client.acquire_lock('/my_lock2', 10)
26
+ index = client.get_lock('/my_lock2', field:'index')
27
+ expect do
28
+ client.renew_lock('/my_lock2', 10, index: index)
29
+ end.to_not raise_error
30
+ end
31
+
32
+ it 'should be able to delete a lock based on value' do
33
+ client.acquire_lock('/my_lock3', 10, value: 123)
34
+ expect do
35
+ client.delete_lock('/my_lock3', value: 123)
36
+ end.to_not raise_error
37
+ end
38
+
39
+ it 'should be able to delete a lock based on index' do
40
+ client.acquire_lock('/my_lock4', 10)
41
+ index = client.get_lock('/my_lock4', field:'index')
42
+ expect do
43
+ client.delete_lock('/my_lock4', index: index)
44
+ end.to_not raise_error
45
+ end
46
+ end
@@ -0,0 +1,27 @@
1
+ require 'spec_helper'
2
+
3
+ describe Etcd::Node do
4
+
5
+ let(:client) do
6
+ Etcd.client
7
+ end
8
+
9
+ it "should create a directory with parent key when nested keys are set" do
10
+ parent = random_key
11
+ child = random_key
12
+ value = uuid.generate
13
+ client.set(parent+child, value)
14
+ expect(client.get(parent+child)).to_not be_directory
15
+ expect(client.get(parent)).to be_directory
16
+ end
17
+
18
+ context '#children' do
19
+ it 'should raise exception when invoked against a leaf node' do
20
+ parent = random_key
21
+ client.create(random_key, 10)
22
+ expect do
23
+ client.get(random_key).children
24
+ end.to raise_error
25
+ end
26
+ end
27
+ end
@@ -1,4 +1,13 @@
1
- shared_examples "read only client" do
1
+ # Encoding: utf-8
2
+
3
+ require 'spec_helper'
4
+
5
+ describe "Etcd read only client" do
6
+
7
+ let(:client) do
8
+ Etcd.client
9
+ end
10
+
2
11
  it "should not allow write" do
3
12
  key= random_key
4
13
  expect{
@@ -17,7 +26,7 @@ shared_examples "read only client" do
17
26
  it "should allow watch" do
18
27
  key = random_key
19
28
  value = uuid.generate
20
- index = client.set(key, value).index
29
+ index = client.set(key, value).node.modified_index
21
30
  expect(read_only_client.watch(key, index: index).value).to eq(value)
22
31
  end
23
32
  end
@@ -0,0 +1,312 @@
1
+ # Encoding: utf-8
2
+
3
+ require 'spec_helper'
4
+
5
+ describe 'Etcd specs for the main etcd README examples' do
6
+
7
+ let(:client) do
8
+ Etcd.client
9
+ end
10
+
11
+ shared_examples 'response with valid node data' do |action|
12
+ if action == :delete
13
+ it 'should not have value' do
14
+ expect(@response.node.value).to be_nil
15
+ end
16
+ else
17
+ it 'should set the value correctly' do
18
+ expect(@response.node.value).to eq('PinkFloyd')
19
+ end
20
+ end
21
+ if action == :create
22
+ it 'should set the parent key correctly' do
23
+ expect(@response.node.key).to match /^\/queue\/+/
24
+ end
25
+ else
26
+ it 'should set the key properly' do
27
+ expect(@response.node.key).to eq('/message')
28
+ end
29
+ end
30
+
31
+ it 'modified index should be a positive integer' do
32
+ expect(@response.node.created_index).to be > 0
33
+ end
34
+
35
+ it 'created index should be a positive integer' do
36
+ expect(@response.node.modified_index).to be > 0
37
+ end
38
+ end
39
+
40
+ shared_examples 'response with valid http headers' do
41
+
42
+ it 'should have a positive etcd index (comes from http header)' do
43
+ expect(@response.etcd_index).to be > 0
44
+ end
45
+
46
+ it 'should have a positive raft index (comes from http header)' do
47
+ expect(@response.raft_index).to be > 0
48
+ end
49
+
50
+ it 'should have a positive raft term (comes from http header)' do
51
+ expect(@response.raft_term).to be >= 0
52
+ end
53
+ end
54
+
55
+ context 'set a key named "/message"' do
56
+
57
+
58
+ before(:all) do
59
+ @response = Etcd.client.set('/message', 'PinkFloyd')
60
+ end
61
+
62
+ it_should_behave_like 'response with valid http headers'
63
+ it_should_behave_like 'response with valid node data'
64
+
65
+ it 'should set the return action to SET' do
66
+ expect(@response.action).to eq('set')
67
+ end
68
+ end
69
+
70
+ context 'get a key named "/message"' do
71
+
72
+ before(:all) do
73
+ Etcd.client.set('/message', 'PinkFloyd')
74
+ @response = Etcd.client.get('/message')
75
+ end
76
+
77
+ it_should_behave_like 'response with valid http headers'
78
+ it_should_behave_like 'response with valid node data'
79
+
80
+ it 'should set the return action to GET' do
81
+ expect(@response.action).to eq('get')
82
+ end
83
+ end
84
+
85
+ context 'change the value of a key named "/message"' do
86
+
87
+ before(:all) do
88
+ Etcd.client.set('/message', 'World')
89
+ @response = Etcd.client.set('/message','PinkFloyd')
90
+ end
91
+
92
+ it_should_behave_like 'response with valid http headers'
93
+ it_should_behave_like 'response with valid node data'
94
+
95
+ it 'should set the return action to SET' do
96
+ expect(@response.action).to eq('set')
97
+ end
98
+ end
99
+
100
+ context 'delete a key named "/message"' do
101
+
102
+ before(:all) do
103
+ Etcd.client.set('/message', 'World')
104
+ Etcd.client.set('/message','PinkFloyd')
105
+ @response = Etcd.client.delete('/message')
106
+ end
107
+
108
+ it 'should set the return action to SET' do
109
+ expect(@response.action).to eq('delete')
110
+ end
111
+
112
+ it_should_behave_like 'response with valid http headers'
113
+ it_should_behave_like 'response with valid node data', :delete
114
+ end
115
+
116
+ context 'using ttl a key named "/message"' do
117
+
118
+ before(:all) do
119
+ Etcd.client.set('/message', 'World')
120
+ @set_time = Time.now
121
+ @response = Etcd.client.set('/message','PinkFloyd', 5)
122
+ end
123
+
124
+ it_should_behave_like 'response with valid http headers'
125
+ it_should_behave_like 'response with valid node data'
126
+
127
+ it 'should set the return action to SET' do
128
+ expect(@response.action).to eq('set')
129
+ end
130
+
131
+ it 'should have valid expiration time' do
132
+ expect(@response.node.expiration).to_not be_nil
133
+ end
134
+
135
+ it 'should have ttl available from the node' do
136
+ expect(@response.node.ttl).to eq(5)
137
+ end
138
+
139
+ it 'should throw exception after the expiration time' do
140
+ sleep 8
141
+ expect do
142
+ Etcd.client.get('/message')
143
+ end.to raise_error
144
+ end
145
+
146
+ end
147
+
148
+ context 'waiting for a change against a key named "/message"' do
149
+
150
+ before(:all) do
151
+ Etcd.client.set('/message', 'foo')
152
+ thr = Thread.new do
153
+ @response = Etcd.client.watch('/message')
154
+ end
155
+ Etcd.client.set('/message', 'PinkFloyd')
156
+ thr.join
157
+ end
158
+
159
+ it_should_behave_like 'response with valid http headers'
160
+ it_should_behave_like 'response with valid node data'
161
+
162
+ it 'should set the return action to SET' do
163
+ expect(@response.action).to eq('set')
164
+ end
165
+
166
+ it 'should get the exact value by specifying a waitIndex' do
167
+ client.set('/message', 'someshit')
168
+ w_response = client.watch('/message', index: @response.node.modified_index)
169
+ expect(w_response.node.value).to eq('PinkFloyd')
170
+ end
171
+ end
172
+
173
+ context 'atomic in-order keys' do
174
+
175
+ before(:all) do
176
+ @response = Etcd.client.create_in_order('/queue', 'PinkFloyd')
177
+ end
178
+
179
+ it_should_behave_like 'response with valid http headers'
180
+ it_should_behave_like 'response with valid node data', :create
181
+
182
+ it 'should set the return action to create' do
183
+ expect(@response.action).to eq('create')
184
+ end
185
+
186
+ it 'should have the child key as a positive integer' do
187
+ expect(@response.key.split('/').last.to_i).to be > 0
188
+ end
189
+
190
+ it 'should have the child keys as monotonically increasing' do
191
+ first_response = client.create_in_order('/queue', 'The Jimi Hendrix Experience')
192
+ second_response = client.create_in_order('/queue', 'The Doors')
193
+ first_key = first_response.key.split('/').last.to_i
194
+ second_key = second_response.key.split('/').last.to_i
195
+ expect(first_key).to be < second_key
196
+ end
197
+
198
+ it 'should enlist all children in sorted manner' do
199
+ responses = []
200
+ 10.times do |n|
201
+ responses << client.create_in_order('/queue', 'Deep Purple - Track #{n}')
202
+ end
203
+ directory = client.get('/queue', sorted: true)
204
+ past_index = directory.children.index(responses.first.node)
205
+ 9.times do |n|
206
+ current_index = directory.children.index(responses[n+1].node)
207
+ expect(current_index).to be > past_index
208
+ past_index = current_index
209
+ end
210
+ end
211
+ end
212
+
213
+ context 'directory with ttl' do
214
+
215
+ before(:all) do
216
+ @response = Etcd.client.set('/directory', dir: true, ttl: 4)
217
+ end
218
+
219
+ it 'should create a directory' do
220
+ expect(client.get('/directory')).to be_directory
221
+ end
222
+
223
+ it 'should have valid expiration time' do
224
+ expect(client.get('/directory').node.expiration).to_not be_nil
225
+ end
226
+
227
+ it 'should have pre-designated ttl' do
228
+ expect(client.get('/directory').node.ttl).to eq(4)
229
+ end
230
+
231
+ it 'will throw error if updated without setting prevExist' do
232
+ expect do
233
+ client.set('/directory', dir:true, ttl:5)
234
+ end.to raise_error
235
+ end
236
+
237
+ it 'can be updated by setting prevExist to true' do
238
+ client.set('/directory', prevExist: true, dir:true, ttl:5)
239
+ expect(client.get('/directory').node.ttl).to eq(5)
240
+ end
241
+
242
+ it 'watchers should get expriy notification' do
243
+ client.set('/directory/a', 'Test')
244
+ client.set('/directory', prevExist: true, dir:true, ttl:2)
245
+ response = client.watch('/directory/a', consistent: true, timeout: 3)
246
+ expect(response.action).to eq('expire')
247
+ end
248
+ it 'should be expired after ttl' do
249
+ sleep 5
250
+ expect do
251
+ client.get('/directory')
252
+ end.to raise_error
253
+ end
254
+ end
255
+
256
+ context 'atomic compare and swap' do
257
+
258
+ it 'should raise error if prevExist is passed a false' do
259
+ client.set('/foo', 'one')
260
+ expect do
261
+ client.set('/foo','three', prevExist: false)
262
+ end.to raise_error
263
+ end
264
+
265
+ it 'should raise error is prevValue is wrong' do
266
+ client.set('/foo', 'one')
267
+ expect do
268
+ client.set('/foo','three', prevValue: 'two')
269
+ end.to raise_error
270
+ end
271
+
272
+ it 'should allow setting the value when prevValue is right' do
273
+ client.set('/foo', 'one')
274
+ expect(client.set('/foo','three', prevValue: 'one').value).to eq('three')
275
+ end
276
+ end
277
+ context 'directory manipulation' do
278
+ it 'should allow creating directory' do
279
+ expect(client.set('/dir',dir:true)).to be_directory
280
+ end
281
+
282
+ it 'should allow listing directory' do
283
+ client.set('/foo_dir/foo','bar')
284
+ expect(client.get('/').children.map(&:key)).to include('/foo_dir')
285
+ end
286
+
287
+ it 'should allow recursive directory listing' do
288
+ response = client.get('/', recursive: true)
289
+ expect(response.children.find{|n|n.key=='/foo_dir'}.children).to_not be_empty
290
+ end
291
+
292
+ it 'should be able to delete empty directory without the recusrive flag' do
293
+ expect(client.delete('/dir', dir: true).action).to eq('delete')
294
+ end
295
+
296
+ it 'should be able to delete directory with children with the recusrive flag' do
297
+ expect(client.delete('/foo_dir', recursive: true).action).to eq('delete')
298
+ end
299
+ end
300
+
301
+ context 'hidden nodes' do
302
+
303
+ before(:all) do
304
+ Etcd.client.set('/_message', 'Hello Hidden World')
305
+ Etcd.client.set('/message', 'Hello World')
306
+ end
307
+
308
+ it 'should not be visible in directory listing' do
309
+ expect(client.get('/').children.map(&:key)).to_not include('_message')
310
+ end
311
+ end
312
+ end