etcd 0.0.6 → 0.2.0.alpha

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,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