etcd-rb 1.0.0.pre0 → 1.0.0.pre1
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +22 -2
- data/lib/etcd/client.rb +80 -11
- data/lib/etcd/version.rb +1 -1
- data/lib/etcd.rb +2 -0
- data/spec/etcd/client_spec.rb +108 -2
- data/spec/integration/etcd_spec.rb +58 -0
- data/spec/spec_helper.rb +20 -1
- metadata +38 -4
data/README.md
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
# Ruby
|
1
|
+
# Ruby [etcd](https://github.com/coreos/etcd) driver
|
2
2
|
|
3
3
|
[![Build Status](https://travis-ci.org/iconara/etcd-rb.png?branch=master)](https://travis-ci.org/iconara/etcd-rb)
|
4
4
|
[![Coverage Status](https://coveralls.io/repos/iconara/etcd-rb/badge.png?branch=master)](https://coveralls.io/r/iconara/etcd-rb)
|
@@ -7,6 +7,8 @@
|
|
7
7
|
|
8
8
|
A modern Ruby, compatible with 1.9.3 or later. Continously tested with MRI 1.9.3, 2.0.0 and JRuby 1.7.x.
|
9
9
|
|
10
|
+
An etcd cluster. _Currently incompatible with the most recent versions of etcd because they return the wrong URI for the leader._
|
11
|
+
|
10
12
|
# Installation
|
11
13
|
|
12
14
|
gem install etcd-rb --prerelease
|
@@ -21,7 +23,25 @@ client.set('/foo', 'bar')
|
|
21
23
|
client.get('/foo')
|
22
24
|
```
|
23
25
|
|
24
|
-
See the full [API documentation](http://rubydoc.info/gems/etcd-rb/frames) for more.
|
26
|
+
See the full [API documentation](http://rubydoc.info/gems/etcd-rb/frames) for more. All core features are supported, including test-and-set, TTL, watches -- as well as a few convenience features like continuous watching.
|
27
|
+
|
28
|
+
# Features
|
29
|
+
|
30
|
+
## Continuous watches: observers
|
31
|
+
|
32
|
+
Most of the time when you use watches with etcd you want to immediately re-watch the key when you get a change notification. The `Client#observe` method handles this for you, including re-watching with the last seen index, so that you don't miss any updates.
|
33
|
+
|
34
|
+
## Automatic leader detection
|
35
|
+
|
36
|
+
You can point the client to any node in the etcd cluster, it will ask that node for the current leader and direct all subsequent requests directly to the leader to avoid unnecessary redirects. When the leader changes, detected by a redirect, the new leader will be registered and used instead of the previous.
|
37
|
+
|
38
|
+
## Automacic failover & retry
|
39
|
+
|
40
|
+
When connecting for the first time, and when the leader changes, the list of nodes in the cluster is cached. Should the node that the client is talking to become unreachable, the client will attempt to connect to the next known node, until it finds one that responds. The first node to respond will be asked for the current leader, which will then be used for subsequent request.
|
41
|
+
|
42
|
+
This is handled completely transparently to you.
|
43
|
+
|
44
|
+
Watches are a special case, since they use long polling, they will break when the leader goes down. Observers will attempt to reestablish their watches with the new leader.
|
25
45
|
|
26
46
|
# Changelog & versioning
|
27
47
|
|
data/lib/etcd/client.rb
CHANGED
@@ -59,13 +59,35 @@ module Etcd
|
|
59
59
|
class Client
|
60
60
|
# Creates a new `etcd` client.
|
61
61
|
#
|
62
|
+
# You should call {#connect} on the client to properly initialize it. The
|
63
|
+
# creation of the client and connection is divided into two parts to avoid
|
64
|
+
# doing network connections in the object initialization code. Many times
|
65
|
+
# you want to defer things with side-effect until the whole object graph
|
66
|
+
# has been created. {#connect} returns self so you can just chain it after
|
67
|
+
# the call to {.new}, e.g. `Client.new.connect`.
|
68
|
+
#
|
69
|
+
# You can specify a seed node to connect to using the `:uri` option (which
|
70
|
+
# defaults to 127.0.0.1:4001), but once connected the client will prefer to
|
71
|
+
# talk to the master in order to avoid unnecessary HTTP requests, and to
|
72
|
+
# make sure that get operations find the most recent value.
|
73
|
+
#
|
62
74
|
# @param [Hash] options
|
63
|
-
# @option options [String] :
|
64
|
-
#
|
75
|
+
# @option options [String] :uri ('http://127.0.0.1:4001') The etcd host and
|
76
|
+
# port to connect to
|
65
77
|
def initialize(options={})
|
66
|
-
@
|
67
|
-
@
|
78
|
+
@seed_uri = options[:uri] || 'http://127.0.0.1:4001'
|
79
|
+
@protocol_version = 'v1'
|
68
80
|
@http_client = HTTPClient.new(agent_name: "etcd-rb/#{VERSION}")
|
81
|
+
@http_client.redirect_uri_callback = method(:handle_redirected)
|
82
|
+
end
|
83
|
+
|
84
|
+
def connect
|
85
|
+
change_uris(@seed_uri)
|
86
|
+
cache_machines
|
87
|
+
change_uris(@machines_cache.first)
|
88
|
+
self
|
89
|
+
rescue AllNodesDownError => e
|
90
|
+
raise ConnectionError, e.message, e.backtrace
|
69
91
|
end
|
70
92
|
|
71
93
|
# Sets the value of a key.
|
@@ -84,7 +106,7 @@ module Etcd
|
|
84
106
|
if ttl = options[:ttl]
|
85
107
|
body[:ttl] = ttl
|
86
108
|
end
|
87
|
-
response =
|
109
|
+
response = request(:post, uri(key), body: body)
|
88
110
|
data = MultiJson.load(response.body)
|
89
111
|
data[S_PREV_VALUE]
|
90
112
|
end
|
@@ -110,7 +132,7 @@ module Etcd
|
|
110
132
|
if ttl = options[:ttl]
|
111
133
|
body[:ttl] = ttl
|
112
134
|
end
|
113
|
-
response =
|
135
|
+
response = request(:post, uri(key), body: body)
|
114
136
|
response.status == 200
|
115
137
|
end
|
116
138
|
|
@@ -123,7 +145,7 @@ module Etcd
|
|
123
145
|
# @return [String, Hash] the value for the key, or a hash of keys and values
|
124
146
|
# when the key is a prefix.
|
125
147
|
def get(key)
|
126
|
-
response =
|
148
|
+
response = request(:get, uri(key))
|
127
149
|
if response.status == 200
|
128
150
|
data = MultiJson.load(response.body)
|
129
151
|
if data.is_a?(Array)
|
@@ -154,7 +176,7 @@ module Etcd
|
|
154
176
|
# @return [Hash] a with info about the key, the exact contents depend on
|
155
177
|
# what kind of key it is.
|
156
178
|
def info(key)
|
157
|
-
response =
|
179
|
+
response = request(:get, uri(key))
|
158
180
|
if response.status == 200
|
159
181
|
data = MultiJson.load(response.body)
|
160
182
|
if data.is_a?(Array)
|
@@ -180,7 +202,7 @@ module Etcd
|
|
180
202
|
# @param key [String] the key to remove
|
181
203
|
# @return [String] the previous value, if any
|
182
204
|
def delete(key)
|
183
|
-
response =
|
205
|
+
response = request(:delete, uri(key))
|
184
206
|
if response.status == 200
|
185
207
|
data = MultiJson.load(response.body)
|
186
208
|
data[S_PREV_VALUE]
|
@@ -228,7 +250,7 @@ module Etcd
|
|
228
250
|
if index = options[:index]
|
229
251
|
parameters[:index] = index
|
230
252
|
end
|
231
|
-
response =
|
253
|
+
response = request(:get, uri(prefix, S_WATCH), query: parameters)
|
232
254
|
data = MultiJson.load(response.body)
|
233
255
|
info = extract_info(data)
|
234
256
|
yield info[:value], info[:key], info
|
@@ -268,6 +290,16 @@ module Etcd
|
|
268
290
|
Observer.new(self, prefix, handler).tap(&:run)
|
269
291
|
end
|
270
292
|
|
293
|
+
# Returns a list of URIs for the machines in the `etcd` cluster.
|
294
|
+
#
|
295
|
+
# The first URI is for the leader.
|
296
|
+
#
|
297
|
+
# @return [Array<String>] the URIs of the machines in the cluster
|
298
|
+
def machines
|
299
|
+
response = request(:get, @machines_uri)
|
300
|
+
response.body.split(MACHINES_SEPARATOR_RE)
|
301
|
+
end
|
302
|
+
|
271
303
|
private
|
272
304
|
|
273
305
|
S_KEY = 'key'.freeze
|
@@ -281,12 +313,23 @@ module Etcd
|
|
281
313
|
S_PREV_VALUE = 'prevValue'.freeze
|
282
314
|
S_ACTION = 'action'.freeze
|
283
315
|
S_WATCH = 'watch'.freeze
|
316
|
+
S_LOCATION = 'location'.freeze
|
284
317
|
|
285
318
|
S_SLASH = '/'.freeze
|
319
|
+
MACHINES_SEPARATOR_RE = /,\s*/
|
286
320
|
|
287
321
|
def uri(key, action=S_KEYS)
|
288
322
|
key = "/#{key}" unless key.start_with?(S_SLASH)
|
289
|
-
"
|
323
|
+
"#{@base_uri}/#{action}#{key}"
|
324
|
+
end
|
325
|
+
|
326
|
+
def request(method, uri, args={})
|
327
|
+
@http_client.request(method, uri, args.merge(follow_redirect: true))
|
328
|
+
rescue HTTPClient::TimeoutError => e
|
329
|
+
old_base_uri = @base_uri
|
330
|
+
handle_leader_down
|
331
|
+
uri.sub!(old_base_uri, @base_uri)
|
332
|
+
retry
|
290
333
|
end
|
291
334
|
|
292
335
|
def extract_info(data)
|
@@ -308,6 +351,32 @@ module Etcd
|
|
308
351
|
info
|
309
352
|
end
|
310
353
|
|
354
|
+
def handle_redirected(uri, response)
|
355
|
+
location = URI.parse(response.header[S_LOCATION][0])
|
356
|
+
change_uris("#{location.scheme}://#{location.host}:#{location.port}")
|
357
|
+
cache_machines
|
358
|
+
@http_client.default_redirect_uri_callback(uri, response)
|
359
|
+
end
|
360
|
+
|
361
|
+
def handle_leader_down
|
362
|
+
if @machines_cache && @machines_cache.any?
|
363
|
+
@machines_cache.reject! { |m| @base_uri.include?(m) }
|
364
|
+
change_uris(@machines_cache.shift)
|
365
|
+
else
|
366
|
+
raise AllNodesDownError, 'All known nodes are down'
|
367
|
+
end
|
368
|
+
end
|
369
|
+
|
370
|
+
def cache_machines
|
371
|
+
@machines_cache = machines
|
372
|
+
end
|
373
|
+
|
374
|
+
def change_uris(leader_uri, options={})
|
375
|
+
@base_uri = "#{leader_uri}/#{@protocol_version}"
|
376
|
+
@leader_uri = "#{@base_uri}/leader"
|
377
|
+
@machines_uri = "#{@base_uri}/machines"
|
378
|
+
end
|
379
|
+
|
311
380
|
# @private
|
312
381
|
class Observer
|
313
382
|
def initialize(client, prefix, handler)
|
data/lib/etcd/version.rb
CHANGED
data/lib/etcd.rb
CHANGED
data/spec/etcd/client_spec.rb
CHANGED
@@ -6,7 +6,7 @@ require 'spec_helper'
|
|
6
6
|
module Etcd
|
7
7
|
describe Client do
|
8
8
|
let :client do
|
9
|
-
described_class.new(
|
9
|
+
described_class.new(uri: "http://#{host}:#{port}").connect
|
10
10
|
end
|
11
11
|
|
12
12
|
let :host do
|
@@ -14,13 +14,50 @@ module Etcd
|
|
14
14
|
end
|
15
15
|
|
16
16
|
let :port do
|
17
|
-
|
17
|
+
50_000
|
18
18
|
end
|
19
19
|
|
20
20
|
let :base_uri do
|
21
21
|
"http://#{host}:#{port}/v1"
|
22
22
|
end
|
23
23
|
|
24
|
+
let :machines_uri do
|
25
|
+
"http://#{host}:#{port}/v1/machines"
|
26
|
+
end
|
27
|
+
|
28
|
+
let :leader_uri do
|
29
|
+
"http://#{host}:#{port}/v1/leader"
|
30
|
+
end
|
31
|
+
|
32
|
+
let :machines do
|
33
|
+
%W[http://#{host}:#{port} http://#{host}:#{port + 2} http://#{host}:#{port + 1}]
|
34
|
+
end
|
35
|
+
|
36
|
+
before do
|
37
|
+
stub_request(:get, leader_uri).to_return(body: "http://#{host}:#{port}")
|
38
|
+
stub_request(:get, machines_uri).to_return(body: machines.join(', '))
|
39
|
+
end
|
40
|
+
|
41
|
+
describe '#connect' do
|
42
|
+
it 'gets the list of machines' do
|
43
|
+
client
|
44
|
+
WebMock.should have_requested(:get, machines_uri)
|
45
|
+
end
|
46
|
+
|
47
|
+
it 'raises an error when the seed node cannot be contacted' do
|
48
|
+
stub_request(:get, machines_uri).to_timeout
|
49
|
+
expect { client }.to raise_error(ConnectionError)
|
50
|
+
end
|
51
|
+
|
52
|
+
it 'discards the seed node and instead talks to the leader (the first in the list of machines)' do
|
53
|
+
machines.push(machines.shift)
|
54
|
+
stub_request(:get, machines_uri).to_return(body: machines.join(','))
|
55
|
+
stub_request(:get, "#{base_uri}/keys/foo").to_return(body: MultiJson.dump({'value' => 'wrong value'}))
|
56
|
+
stub_request(:get, "#{machines[0]}/v1/keys/foo").to_return(body: MultiJson.dump({'value' => 'right value'}))
|
57
|
+
client.get('/foo').should == 'right value'
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
24
61
|
describe '#get' do
|
25
62
|
before do
|
26
63
|
stub_request(:get, "#{base_uri}/keys/foo").to_return(body: MultiJson.dump({'value' => 'bar'}))
|
@@ -55,6 +92,66 @@ module Etcd
|
|
55
92
|
client.get('/foo').should eql({'/foo/bar' => 'bar', '/foo/baz' => 'baz'})
|
56
93
|
end
|
57
94
|
end
|
95
|
+
|
96
|
+
context 'when not talking to the master' do
|
97
|
+
before do
|
98
|
+
stub_request(:get, "#{base_uri}/keys/foo").to_return(status: 307, headers: {'Location' => 'http://example.com:7654/v1/keys/foo'})
|
99
|
+
stub_request(:get, 'http://example.com:7654/v1/machines').to_return(body: %w[http://example.com:7654 http://example.com:7655].join(', '))
|
100
|
+
stub_request(:get, 'http://example.com:7654/v1/keys/foo').to_return(body: MultiJson.dump({'value' => 'bar'}))
|
101
|
+
end
|
102
|
+
|
103
|
+
it 'follows redirects' do
|
104
|
+
client.get('/foo').should == 'bar'
|
105
|
+
end
|
106
|
+
|
107
|
+
it 'sets the new host as the new leader when it is redirected' do
|
108
|
+
client.get('/foo')
|
109
|
+
client.get('/foo').should == 'bar'
|
110
|
+
WebMock.should have_requested(:get, "#{base_uri}/keys/foo").once
|
111
|
+
end
|
112
|
+
|
113
|
+
it 'asks for a new list of cluster machines' do
|
114
|
+
client.get('/foo')
|
115
|
+
WebMock.should have_requested(:get, 'http://example.com:7654/v1/machines').once
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
context 'when nodes go down' do
|
120
|
+
let :uris do
|
121
|
+
machines.map { |m| "#{m}/v1/keys/thing" }
|
122
|
+
end
|
123
|
+
|
124
|
+
it 'tries with another node when the leader appears to be down' do
|
125
|
+
stub_request(:get, uris[0]).to_timeout
|
126
|
+
stub_request(:get, uris[1]).to_return(body: MultiJson.dump({'value' => 'bar'}))
|
127
|
+
stub_request(:get, uris[2]).to_return(body: MultiJson.dump({'value' => 'baz'}))
|
128
|
+
client.get('/thing').should == 'bar'
|
129
|
+
end
|
130
|
+
|
131
|
+
it 'tries other nodes until it finds one that responds' do
|
132
|
+
stub_request(:get, uris[0]).to_timeout
|
133
|
+
stub_request(:get, uris[1]).to_timeout
|
134
|
+
stub_request(:get, uris[2]).to_return(body: MultiJson.dump({'value' => 'bar'}))
|
135
|
+
client.get('/thing').should == 'bar'
|
136
|
+
end
|
137
|
+
|
138
|
+
it 'follows redirects when trying a node after a leader failure, and saves the new leader' do
|
139
|
+
stub_request(:get, uris[0]).to_timeout
|
140
|
+
stub_request(:get, uris[1]).to_return(status: 307, headers: {'Location' => uris[2]})
|
141
|
+
stub_request(:get, uris[2]).to_return(body: MultiJson.dump({'value' => 'bar'}))
|
142
|
+
stub_request(:get, uris[2].sub('/keys/thing', '/machines')).to_return(body: [uris[1], uris[2]].join(', '))
|
143
|
+
stub_request(:get, uris[2].sub('/thing', '/bar')).to_return(body: MultiJson.dump({'value' => 'baz'}))
|
144
|
+
client.get('/thing').should == 'bar'
|
145
|
+
client.get('/bar').should == 'baz'
|
146
|
+
end
|
147
|
+
|
148
|
+
it 'raises an error when all leader candidates have been exhausted' do
|
149
|
+
stub_request(:get, uris[0]).to_timeout
|
150
|
+
stub_request(:get, uris[1]).to_timeout
|
151
|
+
stub_request(:get, uris[2]).to_timeout
|
152
|
+
expect { client.get('/thing') }.to raise_error(AllNodesDownError)
|
153
|
+
end
|
154
|
+
end
|
58
155
|
end
|
59
156
|
|
60
157
|
describe '#set' do
|
@@ -341,5 +438,14 @@ module Etcd
|
|
341
438
|
new_keys.should == [true, nil, nil]
|
342
439
|
end
|
343
440
|
end
|
441
|
+
|
442
|
+
describe '#machines' do
|
443
|
+
it 'returns a list of host and ports of the machines in the etcd cluster' do
|
444
|
+
body = 'http://host01:4001, http://host02:4001, http://host03:4001'
|
445
|
+
stub_request(:get, machines_uri).to_return(body: body)
|
446
|
+
stub_request(:get, machines_uri.sub(base_uri, 'http://host01:4001/v1')).to_return(body: body)
|
447
|
+
client.machines.should == %w[http://host01:4001 http://host02:4001 http://host03:4001]
|
448
|
+
end
|
449
|
+
end
|
344
450
|
end
|
345
451
|
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'spec_helper'
|
4
|
+
require 'open-uri'
|
5
|
+
|
6
|
+
|
7
|
+
describe 'A etcd client' do
|
8
|
+
let :client do
|
9
|
+
Etcd::Client.new(uri: ENV['ETCD_URI']).connect
|
10
|
+
end
|
11
|
+
|
12
|
+
let :prefix do
|
13
|
+
"/etcd-rb/#{rand(234234)}"
|
14
|
+
end
|
15
|
+
|
16
|
+
let :key do
|
17
|
+
"#{prefix}/hello"
|
18
|
+
end
|
19
|
+
|
20
|
+
before do
|
21
|
+
WebMock.disable!
|
22
|
+
end
|
23
|
+
|
24
|
+
before do
|
25
|
+
begin
|
26
|
+
open("#{ENV['ETCD_URI']}/v1/leader").read
|
27
|
+
rescue Errno::ECONNREFUSED, Errno::ENOENT
|
28
|
+
pending('etcd not running, start it with `./spec/resources/etcd-cluster start`')
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
before do
|
33
|
+
client.delete(key)
|
34
|
+
end
|
35
|
+
|
36
|
+
it 'sets and gets the value for a key' do
|
37
|
+
client.set(key, 'foo')
|
38
|
+
client.get(key).should == 'foo'
|
39
|
+
end
|
40
|
+
|
41
|
+
it 'sets a key with a TTL' do
|
42
|
+
client.set(key, 'foo', ttl: 5)
|
43
|
+
client.info(key)[:ttl].should be_within(1).of(5)
|
44
|
+
client.info(key)[:expiration].should be_within(1).of(Time.now + 5)
|
45
|
+
end
|
46
|
+
|
47
|
+
it 'watches for changes to a key' do
|
48
|
+
Thread.start { sleep(0.1); client.set(key, 'baz') }
|
49
|
+
new_value = client.watch(key) { |value| value }
|
50
|
+
new_value.should == 'baz'
|
51
|
+
end
|
52
|
+
|
53
|
+
it 'conditionally sets the value for a key' do
|
54
|
+
client.set(key, 'bar')
|
55
|
+
client.update(key, 'qux', 'baz').should be_false
|
56
|
+
client.update(key, 'qux', 'bar').should be_true
|
57
|
+
end
|
58
|
+
end
|
data/spec/spec_helper.rb
CHANGED
@@ -1,5 +1,24 @@
|
|
1
1
|
# encoding: utf-8
|
2
2
|
|
3
3
|
require 'bundler/setup'
|
4
|
+
|
5
|
+
unless ENV['COVERAGE'] == 'no'
|
6
|
+
require 'coveralls'
|
7
|
+
require 'simplecov'
|
8
|
+
|
9
|
+
if ENV.include?('TRAVIS')
|
10
|
+
Coveralls.wear!
|
11
|
+
SimpleCov.formatter = Coveralls::SimpleCov::Formatter
|
12
|
+
end
|
13
|
+
|
14
|
+
SimpleCov.start do
|
15
|
+
add_group 'Source', 'lib'
|
16
|
+
add_group 'Unit tests', 'spec/etcd'
|
17
|
+
add_group 'Integration tests', 'spec/integration'
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
ENV['ETCD_URI'] ||= 'http://127.0.0.1:4001'
|
22
|
+
|
23
|
+
require 'webmock/rspec'
|
4
24
|
require 'etcd'
|
5
|
-
require 'webmock/rspec'
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: etcd-rb
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.0.0.
|
4
|
+
version: 1.0.0.pre1
|
5
5
|
prerelease: 6
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,8 +9,40 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2013-08-
|
13
|
-
dependencies:
|
12
|
+
date: 2013-08-27 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: httpclient
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ~>
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: 2.3.0
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ~>
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: 2.3.0
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
name: multi_json
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
33
|
+
none: false
|
34
|
+
requirements:
|
35
|
+
- - ~>
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: 1.7.0
|
38
|
+
type: :runtime
|
39
|
+
prerelease: false
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ~>
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: 1.7.0
|
14
46
|
description: ''
|
15
47
|
email:
|
16
48
|
- theo@iconara.net
|
@@ -23,6 +55,7 @@ files:
|
|
23
55
|
- lib/etcd.rb
|
24
56
|
- README.md
|
25
57
|
- spec/etcd/client_spec.rb
|
58
|
+
- spec/integration/etcd_spec.rb
|
26
59
|
- spec/spec_helper.rb
|
27
60
|
homepage: http://github.com/iconara/etcd-rb
|
28
61
|
licenses:
|
@@ -45,11 +78,12 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
45
78
|
version: 1.3.1
|
46
79
|
requirements: []
|
47
80
|
rubyforge_project:
|
48
|
-
rubygems_version: 1.8.
|
81
|
+
rubygems_version: 1.8.25
|
49
82
|
signing_key:
|
50
83
|
specification_version: 3
|
51
84
|
summary: ''
|
52
85
|
test_files:
|
53
86
|
- spec/etcd/client_spec.rb
|
87
|
+
- spec/integration/etcd_spec.rb
|
54
88
|
- spec/spec_helper.rb
|
55
89
|
has_rdoc:
|