etcd-rb 1.0.0 → 1.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +4 -4
- data/lib/etcd/client/failover.rb +6 -11
- data/lib/etcd/client/observing.rb +2 -2
- data/lib/etcd/client/protocol.rb +40 -30
- data/lib/etcd/cluster.rb +6 -12
- data/lib/etcd/constants.rb +4 -4
- data/lib/etcd/heartbeat.rb +2 -2
- data/lib/etcd/loggable.rb +1 -1
- data/lib/etcd/node.rb +27 -10
- data/lib/etcd/observer.rb +3 -2
- data/lib/etcd/version.rb +2 -2
- data/spec/etcd/client_spec.rb +51 -49
- data/spec/etcd/cluster_spec.rb +22 -32
- data/spec/etcd/node_spec.rb +24 -12
- data/spec/etcd/observer_spec.rb +48 -48
- data/spec/integration/etcd_spec.rb +29 -16
- data/spec/resources/cluster_controller.rb +2 -2
- data/spec/support/client_helper.rb +6 -2
- data/spec/support/cluster_helper.rb +28 -30
- metadata +3 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 3f44e73b7a78f25139d79210e6802c4837b116e6
|
4
|
+
data.tar.gz: ffbe7d2df48e3cc9e050a127b605d412c2a352f6
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 2b6de922dd29798bb860b69081d8c0e1ba16fc06a89ab985c7f6f7f4e3bc0cb288c045bd65cfbe527fcafbec1ec301a69aee5879b647dcd0e331195620fe207e
|
7
|
+
data.tar.gz: e43e9f2006c706bff16b6a3985d0d554c38b566379f2e87dabe87006b4c6e39980bce142ad4445dc9a216e373cd9d4dc5dfce6ef34177b329ccfe0f01a2dc8f3
|
data/README.md
CHANGED
@@ -5,10 +5,10 @@
|
|
5
5
|
|
6
6
|
# Requirements
|
7
7
|
|
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
8
|
- Linux/OSX OS
|
10
|
-
-
|
11
|
-
|
9
|
+
- A modern Ruby, compatible with 2.0.0 or later. Continously tested with MRI 2.0.0, 2.2 and JRuby 9.0.0.
|
10
|
+
- Etcd 2.0.0 or later (only the V2 API is supported). To have a local etcd binary, run:
|
11
|
+
- `$ sh/install-etcd.sh`
|
12
12
|
|
13
13
|
# Installation
|
14
14
|
|
@@ -214,4 +214,4 @@ _Licensed under the Apache License, Version 2.0 (the "License"); you may not use
|
|
214
214
|
|
215
215
|
[http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0)
|
216
216
|
|
217
|
-
_Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License._
|
217
|
+
_Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License._
|
data/lib/etcd/client/failover.rb
CHANGED
@@ -1,7 +1,5 @@
|
|
1
1
|
module Etcd
|
2
2
|
class Client
|
3
|
-
|
4
|
-
|
5
3
|
# @param options [Hash]
|
6
4
|
# @option options [Array] :uris (['http://127.0.0.1:4001']) seed uris with etcd cluster nodes
|
7
5
|
# @option options [Float] :heartbeat_freq (0.0) check-frequency for leader status (in seconds)
|
@@ -34,7 +32,6 @@ module Etcd
|
|
34
32
|
self
|
35
33
|
end
|
36
34
|
|
37
|
-
|
38
35
|
# Creates a Cluster-instance from `@seed_uris`
|
39
36
|
# and stores the cluster leader information
|
40
37
|
def update_cluster
|
@@ -59,10 +56,9 @@ module Etcd
|
|
59
56
|
end
|
60
57
|
|
61
58
|
def leader_uri
|
62
|
-
leader && leader.
|
59
|
+
leader && leader.client_urls && leader.client_urls.first
|
63
60
|
end
|
64
61
|
|
65
|
-
|
66
62
|
def start_heartbeat_if_needed
|
67
63
|
logger.debug("client - starting heartbeat")
|
68
64
|
@heartbeat = Etcd::Heartbeat.new(self, @heartbeat_freq)
|
@@ -77,27 +73,26 @@ module Etcd
|
|
77
73
|
http_client.default_redirect_uri_callback(uri, response)
|
78
74
|
end
|
79
75
|
|
76
|
+
private
|
80
77
|
|
81
|
-
private
|
82
78
|
# :uri and :request_data are the only methods calling :leader method
|
83
79
|
# so they both need to handle the case for missing leader in cluster
|
84
80
|
def uri(key, action=S_KEYS)
|
85
81
|
raise AllNodesDownError unless leader
|
86
82
|
key = "/#{key}" unless key.start_with?(S_SLASH)
|
87
|
-
"#{leader_uri}/
|
83
|
+
"#{leader_uri}/v2/#{action}#{key}"
|
88
84
|
end
|
89
85
|
|
90
|
-
|
91
86
|
def request_data(method, uri, args={})
|
92
87
|
logger.debug("request_data: #{method} - #{uri} #{args.inspect}")
|
93
88
|
begin
|
94
89
|
super
|
95
90
|
rescue Errno::ECONNREFUSED, HTTPClient::TimeoutError => e
|
96
91
|
logger.debug("request_data: re-election handling")
|
97
|
-
old_leader_uri = @leader.
|
92
|
+
old_leader_uri = @leader.client_urls.first
|
98
93
|
update_cluster
|
99
94
|
if @leader
|
100
|
-
uri = uri.gsub(old_leader_uri, @leader.
|
95
|
+
uri = uri.gsub(old_leader_uri, @leader.client_urls.first)
|
101
96
|
retry
|
102
97
|
else
|
103
98
|
raise AllNodesDownError
|
@@ -106,4 +101,4 @@ private
|
|
106
101
|
end
|
107
102
|
|
108
103
|
end
|
109
|
-
end
|
104
|
+
end
|
@@ -31,8 +31,8 @@ module Etcd
|
|
31
31
|
#
|
32
32
|
# @return [#cancel, #join] an observer object which you can call cancel and
|
33
33
|
# join on
|
34
|
-
def observe(prefix, &handler)
|
35
|
-
ob = Observer.new(self, prefix, handler).tap(&:run)
|
34
|
+
def observe(prefix, options = {}, &handler)
|
35
|
+
ob = Observer.new(self, prefix, handler, options).tap(&:run)
|
36
36
|
@observers[prefix] = ob
|
37
37
|
ob
|
38
38
|
end
|
data/lib/etcd/client/protocol.rb
CHANGED
@@ -1,3 +1,12 @@
|
|
1
|
+
# Implements the etcd V2 client API
|
2
|
+
#
|
3
|
+
# Sample API requests/responses
|
4
|
+
# $ curl -L http://127.0.0.1:4001/v2/keys
|
5
|
+
# {"action":"get","node":{"dir":true,"nodes":[{"key":"/foo","value":"bar","modifiedIndex":22,"createdIndex":22}]}}
|
6
|
+
#
|
7
|
+
# $ curl -L http://127.0.0.1:4001/v2/keys/foo
|
8
|
+
# {"action":"get","node":{"key":"/foo","value":"bar","modifiedIndex":22,"createdIndex":22}}
|
9
|
+
|
1
10
|
module Etcd
|
2
11
|
class Client
|
3
12
|
|
@@ -15,8 +24,8 @@ module Etcd
|
|
15
24
|
def set(key, value, options={})
|
16
25
|
body = {:value => value}
|
17
26
|
body[:ttl] = options[:ttl] if options[:ttl]
|
18
|
-
data = request_data(:
|
19
|
-
data[
|
27
|
+
data = request_data(:PUT, key_uri(key), body: body)
|
28
|
+
data[S_PREV_NODE][S_VALUE] if data && data[S_PREV_NODE]
|
20
29
|
end
|
21
30
|
|
22
31
|
# Gets the value or values for a key.
|
@@ -30,12 +39,12 @@ module Etcd
|
|
30
39
|
def get(key)
|
31
40
|
data = request_data(:get, key_uri(key))
|
32
41
|
return nil unless data
|
33
|
-
if data
|
34
|
-
|
35
|
-
acc[
|
42
|
+
if nodes = data[S_NODE][S_NODES]
|
43
|
+
nodes.each_with_object({}) do |node, acc|
|
44
|
+
acc[node[S_KEY]] = node[S_VALUE]
|
36
45
|
end
|
37
46
|
else
|
38
|
-
data[S_VALUE]
|
47
|
+
data[S_NODE][S_VALUE]
|
39
48
|
end
|
40
49
|
end
|
41
50
|
|
@@ -58,7 +67,7 @@ module Etcd
|
|
58
67
|
def update(key, value, expected_value, options={})
|
59
68
|
body = {:value => value, :prevValue => expected_value}
|
60
69
|
body[:ttl] = options[:ttl] if options[:ttl]
|
61
|
-
data = request_data(:
|
70
|
+
data = request_data(:put, key_uri(key), body: body)
|
62
71
|
!! data
|
63
72
|
end
|
64
73
|
|
@@ -71,7 +80,7 @@ module Etcd
|
|
71
80
|
def delete(key)
|
72
81
|
data = request_data(:delete, key_uri(key))
|
73
82
|
return nil unless data
|
74
|
-
data[
|
83
|
+
data[S_PREV_NODE][S_VALUE]
|
75
84
|
end
|
76
85
|
|
77
86
|
# Returns true if the specified key exists.
|
@@ -102,8 +111,8 @@ module Etcd
|
|
102
111
|
def info(key)
|
103
112
|
data = request_data(:get, uri(key))
|
104
113
|
return nil unless data
|
105
|
-
if data
|
106
|
-
|
114
|
+
if nodes = data[S_NODE][S_NODES]
|
115
|
+
nodes.each_with_object({}) do |d, acc|
|
107
116
|
info = extract_info(d)
|
108
117
|
info.delete(:action)
|
109
118
|
acc[info[:key]] = info
|
@@ -140,14 +149,10 @@ module Etcd
|
|
140
149
|
# @yieldparam [Hash] info the info for the key that changed
|
141
150
|
# @return [Object] the result of the given block
|
142
151
|
def watch(prefix, options={})
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
data = request_data(:get, watch_uri(prefix), query: {})
|
148
|
-
end
|
149
|
-
|
150
|
-
info = extract_info(data)
|
152
|
+
options.merge!(wait: 'true')
|
153
|
+
options.delete(:index) if options.has_key?(:index) && options[:index].nil?
|
154
|
+
data = request_data(:get, watch_uri(prefix), query: options)
|
155
|
+
info = extract_info(data)
|
151
156
|
yield info[:value], info[:key], info
|
152
157
|
end
|
153
158
|
|
@@ -157,30 +162,35 @@ module Etcd
|
|
157
162
|
end
|
158
163
|
|
159
164
|
def watch_uri(key)
|
160
|
-
uri(key,
|
165
|
+
uri(key, S_KEYS)
|
161
166
|
end
|
162
167
|
|
163
168
|
private
|
164
169
|
|
165
170
|
def extract_info(data)
|
171
|
+
if data[S_NODE]
|
172
|
+
node = data[S_NODE]
|
173
|
+
else
|
174
|
+
node = data
|
175
|
+
end
|
176
|
+
return {} unless node
|
166
177
|
info = {
|
167
|
-
:key =>
|
168
|
-
:value =>
|
169
|
-
:index =>
|
178
|
+
:key => node[S_KEY],
|
179
|
+
:value => node[S_VALUE],
|
180
|
+
:index => node[S_INDEX],
|
170
181
|
}
|
171
|
-
expiration_s =
|
172
|
-
ttl =
|
173
|
-
previous_value = data[S_PREV_VALUE]
|
182
|
+
expiration_s = node[S_EXPIRATION]
|
183
|
+
ttl = node[S_TTL]
|
174
184
|
action_s = data[S_ACTION]
|
185
|
+
previous_node = data[S_PREV_NODE]
|
175
186
|
info[:expiration] = Time.iso8601(expiration_s) if expiration_s
|
176
187
|
info[:ttl] = ttl if ttl
|
177
|
-
info[:
|
178
|
-
info[:
|
179
|
-
info[:previous_value] = previous_value if previous_value
|
188
|
+
info[:dir] = node[S_DIR] if node.include?(S_DIR)
|
189
|
+
info[:previous_value] = previous_node[S_VALUE] if previous_node
|
180
190
|
info[:action] = action_s.downcase.to_sym if action_s
|
191
|
+
info[:new_key] = !data[S_PREV_NODE]
|
181
192
|
info
|
182
193
|
end
|
183
194
|
|
184
|
-
|
185
195
|
end
|
186
|
-
end
|
196
|
+
end
|
data/lib/etcd/cluster.rb
CHANGED
@@ -24,21 +24,15 @@ module Etcd
|
|
24
24
|
end
|
25
25
|
|
26
26
|
def status_uri(uri)
|
27
|
-
"#{uri}/
|
27
|
+
"#{uri}/v2/members/"
|
28
28
|
end
|
29
29
|
|
30
30
|
def parse_cluster_status(cluster_status_response)
|
31
|
-
cluster_status_response.map do |attrs|
|
32
|
-
|
33
|
-
urls = attrs[S_VALUE].split(S_AND)
|
34
|
-
etcd = urls.grep(/etcd/).first.split("=").last
|
35
|
-
raft = urls.grep(/raft/).first.split("=").last
|
36
|
-
{:name => node_name, :raft => raft, :etcd => etcd}
|
31
|
+
cluster_status_response['members'].map do |attrs|
|
32
|
+
Etcd::Node.parse_node_data(attrs)
|
37
33
|
end
|
38
34
|
end
|
39
35
|
|
40
|
-
|
41
|
-
|
42
36
|
# @example
|
43
37
|
# Etcd::Cluster.nodes_from_uri("http://127.0.0.1:4001")
|
44
38
|
#
|
@@ -50,8 +44,8 @@ module Etcd
|
|
50
44
|
end
|
51
45
|
|
52
46
|
def nodes_from_attributes(node_attributes)
|
53
|
-
res = node_attributes.map do |
|
54
|
-
Etcd::Node.new(
|
47
|
+
res = node_attributes.map do |attrs|
|
48
|
+
Etcd::Node.new(attrs)
|
55
49
|
end
|
56
50
|
end
|
57
51
|
|
@@ -95,4 +89,4 @@ module Etcd
|
|
95
89
|
nodes.select{|x| x.is_leader}.first
|
96
90
|
end
|
97
91
|
end
|
98
|
-
end
|
92
|
+
end
|
data/lib/etcd/constants.rb
CHANGED
@@ -1,18 +1,18 @@
|
|
1
1
|
module Etcd
|
2
2
|
module Constants
|
3
|
+
S_NODE = 'node'.freeze
|
4
|
+
S_NODES = 'nodes'.freeze
|
3
5
|
S_KEY = 'key'.freeze
|
4
6
|
S_KEYS = 'keys'.freeze
|
5
7
|
S_VALUE = 'value'.freeze
|
6
8
|
S_INDEX = 'index'.freeze
|
7
9
|
S_EXPIRATION = 'expiration'.freeze
|
8
10
|
S_TTL = 'ttl'.freeze
|
9
|
-
S_NEW_KEY = 'newKey'.freeze
|
10
11
|
S_DIR = 'dir'.freeze
|
11
|
-
|
12
|
+
S_PREV_NODE = 'prevNode'.freeze
|
12
13
|
S_ACTION = 'action'.freeze
|
13
|
-
S_WATCH = 'watch'.freeze
|
14
14
|
S_LOCATION = 'location'.freeze
|
15
15
|
S_SLASH = '/'.freeze
|
16
16
|
S_AND = '&'.freeze
|
17
17
|
end
|
18
|
-
end
|
18
|
+
end
|
data/lib/etcd/heartbeat.rb
CHANGED
@@ -13,7 +13,7 @@ module Etcd
|
|
13
13
|
# Initiates heartbeating the leader node in a background thread
|
14
14
|
# ensures, that observers are refreshed after leader re-election
|
15
15
|
def start_heartbeat_if_needed
|
16
|
-
logger.debug
|
16
|
+
logger.debug("start_heartbeat_if_needed - enter")
|
17
17
|
return if freq == 0
|
18
18
|
return if @heartbeat_thread
|
19
19
|
@heartbeat_thread = Thread.new do
|
@@ -41,4 +41,4 @@ module Etcd
|
|
41
41
|
end
|
42
42
|
|
43
43
|
end
|
44
|
-
end
|
44
|
+
end
|
data/lib/etcd/loggable.rb
CHANGED
data/lib/etcd/node.rb
CHANGED
@@ -2,44 +2,61 @@ module Etcd
|
|
2
2
|
class Node
|
3
3
|
include Etcd::Constants
|
4
4
|
include Etcd::Requestable
|
5
|
-
attr_accessor :name, :
|
5
|
+
attr_accessor :name, :id, :peer_urls, :client_urls
|
6
6
|
# possible values: :unknown, :running, :down
|
7
7
|
attr_accessor :status
|
8
8
|
attr_accessor :is_leader
|
9
9
|
|
10
|
+
class << self
|
11
|
+
def parse_node_data(attrs)
|
12
|
+
{
|
13
|
+
:id => attrs["id"],
|
14
|
+
:name => attrs["name"],
|
15
|
+
:peer_urls => attrs["peerURLs"],
|
16
|
+
:client_urls => attrs["clientURLs"]
|
17
|
+
}
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
10
21
|
def initialize(opts={})
|
11
22
|
check_required(opts)
|
12
|
-
@name
|
13
|
-
@
|
14
|
-
@
|
23
|
+
@name = opts[:name]
|
24
|
+
@id = opts[:id]
|
25
|
+
@peer_urls = opts[:peer_urls]
|
26
|
+
@client_urls = opts[:client_urls]
|
15
27
|
@status = :unknown
|
16
28
|
end
|
17
29
|
|
18
30
|
def check_required(opts)
|
19
|
-
raise ArgumentError, "
|
31
|
+
raise ArgumentError, "Client URL is required!" unless opts[:client_urls] && opts[:client_urls].any?
|
32
|
+
raise ArgumentError, "Node ID is required!" unless opts[:id]
|
20
33
|
end
|
21
34
|
|
22
35
|
def update_status
|
23
36
|
begin
|
24
|
-
|
25
|
-
@status
|
26
|
-
@is_leader
|
37
|
+
leader_data = request_data(:get, leader_uri)
|
38
|
+
@status = :running
|
39
|
+
@is_leader = (leader_data["id"] == @id)
|
27
40
|
rescue HTTPClient::TimeoutError, Errno::ECONNREFUSED => e
|
28
41
|
@status = :down
|
29
42
|
end
|
30
43
|
end
|
31
44
|
|
32
45
|
def leader_uri
|
33
|
-
"#{@
|
46
|
+
"#{@client_urls.first}/v2/members/leader"
|
34
47
|
end
|
35
48
|
|
36
49
|
def inspect
|
37
|
-
%Q(<#{self.class} - #{name_with_status} - #{
|
50
|
+
%Q(<#{self.class} - #{id} - #{name_with_status} - #{peer_urls}>)
|
38
51
|
end
|
39
52
|
|
40
53
|
def name_with_status
|
41
54
|
print_status = @is_leader ? "leader" : status
|
42
55
|
"#{name} (#{print_status})"
|
43
56
|
end
|
57
|
+
|
58
|
+
def to_json
|
59
|
+
{ :name => name, :id => id, :client_urls => client_urls, :peer_urls => peer_urls }.to_json
|
60
|
+
end
|
44
61
|
end
|
45
62
|
end
|
data/lib/etcd/observer.rb
CHANGED
@@ -2,10 +2,11 @@ module Etcd
|
|
2
2
|
class Observer
|
3
3
|
include Etcd::Loggable
|
4
4
|
|
5
|
-
def initialize(client, prefix, handler)
|
5
|
+
def initialize(client, prefix, handler, options = {})
|
6
6
|
@client = client
|
7
7
|
@prefix = prefix
|
8
8
|
@handler = handler
|
9
|
+
@options = options
|
9
10
|
@index = nil
|
10
11
|
reset_logger!(Logger::DEBUG)
|
11
12
|
end
|
@@ -15,7 +16,7 @@ module Etcd
|
|
15
16
|
@thread = Thread.start do
|
16
17
|
while @running
|
17
18
|
logger.debug "********* watching #{@prefix} with index #{@index}"
|
18
|
-
@client.watch(@prefix, index: @index) do |value, key, info|
|
19
|
+
@client.watch(@prefix, @options.merge(index: @index)) do |value, key, info|
|
19
20
|
if @running
|
20
21
|
logger.debug "watch fired for #{@prefix} with #{info.inspect} "
|
21
22
|
call_handler_in_needed(value, key, info)
|
data/lib/etcd/version.rb
CHANGED
data/spec/etcd/client_spec.rb
CHANGED
@@ -2,12 +2,13 @@
|
|
2
2
|
require 'spec_helper'
|
3
3
|
|
4
4
|
module Etcd
|
5
|
+
|
5
6
|
describe Client do
|
6
7
|
include ClusterHelper
|
7
8
|
include ClientHelper
|
8
9
|
|
9
10
|
def base_uri
|
10
|
-
"http://127.0.0.1:4001/
|
11
|
+
"http://127.0.0.1:4001/v2"
|
11
12
|
end
|
12
13
|
|
13
14
|
let :client do
|
@@ -16,7 +17,7 @@ module Etcd
|
|
16
17
|
|
17
18
|
describe '#get' do
|
18
19
|
before do
|
19
|
-
stub_request(:get, "#{base_uri}/keys/foo").to_return(body: MultiJson.dump({'value' => 'bar'}))
|
20
|
+
stub_request(:get, "#{base_uri}/keys/foo").to_return(body: MultiJson.dump({'node' => {'value' => 'bar'}}))
|
20
21
|
end
|
21
22
|
|
22
23
|
it 'sends a GET request to retrieve the value for a key' do
|
@@ -30,7 +31,7 @@ module Etcd
|
|
30
31
|
end
|
31
32
|
|
32
33
|
it 'parses the response and returns the value' do
|
33
|
-
client.get('/foo').should
|
34
|
+
client.get('/foo').should eq('bar')
|
34
35
|
end
|
35
36
|
|
36
37
|
it 'returns nil if when the key does not exist' do
|
@@ -40,10 +41,10 @@ module Etcd
|
|
40
41
|
|
41
42
|
context 'when listing a prefix' do
|
42
43
|
it 'returns a hash of keys and their values' do
|
43
|
-
values = [
|
44
|
+
values = {'node' => {'nodes' => [
|
44
45
|
{'key' => '/foo/bar', 'value' => 'bar'},
|
45
|
-
{'key' => '/foo/baz', 'value' => 'baz'}
|
46
|
-
]
|
46
|
+
{'key' => '/foo/baz', 'value' => 'baz'}
|
47
|
+
]}}
|
47
48
|
stub_request(:get, "#{base_uri}/keys/foo").to_return(body: MultiJson.dump(values))
|
48
49
|
client.get('/foo').should eql({'/foo/bar' => 'bar', '/foo/baz' => 'baz'})
|
49
50
|
end
|
@@ -53,65 +54,65 @@ module Etcd
|
|
53
54
|
|
54
55
|
describe '#set' do
|
55
56
|
before do
|
56
|
-
stub_request(:
|
57
|
+
stub_request(:put, "#{base_uri}/keys/foo").to_return(body: MultiJson.dump({'prevNode' => {'value' => 1}}))
|
57
58
|
end
|
58
59
|
|
59
|
-
it 'sends a
|
60
|
+
it 'sends a PUT request to set the value for a key' do
|
60
61
|
client.set('/foo', 'bar')
|
61
|
-
WebMock.should have_requested(:
|
62
|
+
WebMock.should have_requested(:put, "#{base_uri}/keys/foo").with(body: 'value=bar')
|
62
63
|
end
|
63
64
|
|
64
65
|
it 'prepends a slash to keys when necessary' do
|
65
66
|
client.set('foo', 'bar')
|
66
|
-
WebMock.should have_requested(:
|
67
|
+
WebMock.should have_requested(:put, "#{base_uri}/keys/foo").with(body: 'value=bar')
|
67
68
|
end
|
68
69
|
|
69
70
|
it 'parses the response and returns the previous value' do
|
70
|
-
stub_request(:
|
71
|
-
client.set('/foo', 'bar').should
|
71
|
+
stub_request(:put, "#{base_uri}/keys/foo").to_return(body: MultiJson.dump({'prevNode' => {'value' => 'baz'}}))
|
72
|
+
client.set('/foo', 'bar').should eq('baz')
|
72
73
|
end
|
73
74
|
|
74
75
|
it 'returns nil when there is no previous value' do
|
75
|
-
stub_request(:
|
76
|
+
stub_request(:put, "#{base_uri}/keys/foo").to_return(body: MultiJson.dump({}))
|
76
77
|
client.set('/foo', 'bar').should be_nil
|
77
78
|
end
|
78
79
|
|
79
80
|
it 'sets a TTL when the :ttl option is given' do
|
80
81
|
client.set('/foo', 'bar', ttl: 3)
|
81
|
-
WebMock.should have_requested(:
|
82
|
+
WebMock.should have_requested(:put, "#{base_uri}/keys/foo").with(body: 'value=bar&ttl=3')
|
82
83
|
end
|
83
84
|
end
|
84
85
|
|
85
86
|
describe '#update' do
|
86
87
|
before do
|
87
|
-
stub_request(:
|
88
|
+
stub_request(:put, "#{base_uri}/keys/foo").to_return(body: MultiJson.dump({}))
|
88
89
|
end
|
89
90
|
|
90
91
|
it 'sends a POST request to set the value conditionally' do
|
91
92
|
client.update('/foo', 'bar', 'baz')
|
92
|
-
WebMock.should have_requested(:
|
93
|
+
WebMock.should have_requested(:put, "#{base_uri}/keys/foo").with(body: 'value=bar&prevValue=baz')
|
93
94
|
end
|
94
95
|
|
95
96
|
it 'returns true when the key is successfully changed' do
|
96
|
-
stub_request(:
|
97
|
+
stub_request(:put, "#{base_uri}/keys/foo").to_return(body: MultiJson.dump({}))
|
97
98
|
client.update('/foo', 'bar', 'baz').should eq(true)
|
98
99
|
end
|
99
100
|
|
100
101
|
it 'returns false when an error is returned' do
|
101
|
-
stub_request(:
|
102
|
+
stub_request(:put, "#{base_uri}/keys/foo").to_return(status: 400, body: MultiJson.dump({}))
|
102
103
|
client.update('/foo', 'bar', 'baz').should eq(false)
|
103
104
|
end
|
104
105
|
|
105
106
|
it 'sets a TTL when the :ttl option is given' do
|
106
107
|
client.update('/foo', 'bar', 'baz', ttl: 3)
|
107
|
-
WebMock.should have_requested(:
|
108
|
+
WebMock.should have_requested(:put, "#{base_uri}/keys/foo").with(body: 'value=bar&prevValue=baz&ttl=3')
|
108
109
|
end
|
109
110
|
end
|
110
111
|
|
111
112
|
|
112
113
|
describe '#delete' do
|
113
114
|
before do
|
114
|
-
stub_request(:delete, "#{base_uri}/keys/foo").to_return(body: MultiJson.dump({}))
|
115
|
+
stub_request(:delete, "#{base_uri}/keys/foo").to_return(body: MultiJson.dump({'prevNode' => {'value' => 1}}))
|
115
116
|
end
|
116
117
|
|
117
118
|
it 'sends a DELETE request to remove a key' do
|
@@ -120,8 +121,8 @@ module Etcd
|
|
120
121
|
end
|
121
122
|
|
122
123
|
it 'returns the previous value' do
|
123
|
-
stub_request(:delete, "#{base_uri}/keys/foo").to_return(body: MultiJson.dump({'
|
124
|
-
client.delete('/foo').should
|
124
|
+
stub_request(:delete, "#{base_uri}/keys/foo").to_return(body: MultiJson.dump({'prevNode' => {'value' => 'bar'}}))
|
125
|
+
client.delete('/foo').should eq('bar')
|
125
126
|
end
|
126
127
|
|
127
128
|
it 'returns nil when there is no previous value' do
|
@@ -132,7 +133,7 @@ module Etcd
|
|
132
133
|
|
133
134
|
describe '#exists?' do
|
134
135
|
it 'returns true if the key has a value' do
|
135
|
-
stub_request(:get, "#{base_uri}/keys/foo").to_return(body: MultiJson.dump({'value' => 'bar'}))
|
136
|
+
stub_request(:get, "#{base_uri}/keys/foo").to_return(body: MultiJson.dump({'node' => {'value' => 'bar'}}))
|
136
137
|
client.exists?('/foo').should eq(true)
|
137
138
|
end
|
138
139
|
|
@@ -144,32 +145,32 @@ module Etcd
|
|
144
145
|
|
145
146
|
describe '#info' do
|
146
147
|
it 'returns the key, value, index, expiration and TTL for a key' do
|
147
|
-
body = MultiJson.dump({'action' => '
|
148
|
+
body = MultiJson.dump({'action' => 'get', 'node' => {'key' => '/foo', 'value' => 'bar', 'index' => 31, 'expiration' => '2013-12-11T12:09:08.123+02:00', 'ttl' => 7}})
|
148
149
|
stub_request(:get, "#{base_uri}/keys/foo").to_return(body: body)
|
149
150
|
info = client.info('/foo')
|
150
|
-
info[:key].should
|
151
|
-
info[:value].should
|
152
|
-
info[:index].should
|
151
|
+
info[:key].should eq('/foo')
|
152
|
+
info[:value].should eq('bar')
|
153
|
+
info[:index].should eq(31)
|
153
154
|
# rounding because of ruby 2.0 time parsing bug @see https://gist.github.com/mindreframer/6746829
|
154
|
-
info[:expiration].to_f.round.should
|
155
|
-
info[:ttl].should
|
155
|
+
info[:expiration].to_f.round.should eq((Time.utc(2013, 12, 11, 10, 9, 8) + 0.123).to_f.round)
|
156
|
+
info[:ttl].should eq(7)
|
156
157
|
end
|
157
158
|
|
158
159
|
it 'returns the dir flag' do
|
159
|
-
body = MultiJson.dump({'action' => '
|
160
|
+
body = MultiJson.dump({'action' => 'get', 'node' => {'key' => '/foo', 'dir' => true}})
|
160
161
|
stub_request(:get, "#{base_uri}/keys/foo").to_return(body: body)
|
161
162
|
info = client.info('/foo')
|
162
|
-
info[:key].should
|
163
|
+
info[:key].should eq('/foo')
|
163
164
|
info[:dir].should eq(true)
|
164
165
|
end
|
165
166
|
|
166
167
|
it 'returns only the pieces of information that are returned' do
|
167
|
-
body = MultiJson.dump({'action' => '
|
168
|
+
body = MultiJson.dump({'action' => 'get', 'node' => {'key' => '/foo', 'value' => 'bar', 'index' => 31}})
|
168
169
|
stub_request(:get, "#{base_uri}/keys/foo").to_return(body: body)
|
169
170
|
info = client.info('/foo')
|
170
|
-
info[:key].should
|
171
|
-
info[:value].should
|
172
|
-
info[:index].should
|
171
|
+
info[:key].should eq('/foo')
|
172
|
+
info[:value].should eq('bar')
|
173
|
+
info[:index].should eq(31)
|
173
174
|
end
|
174
175
|
|
175
176
|
it 'returns nil when the key does not exist' do
|
@@ -179,18 +180,19 @@ module Etcd
|
|
179
180
|
|
180
181
|
context 'when listing a prefix' do
|
181
182
|
it 'returns a hash of keys and their info' do
|
182
|
-
body = MultiJson.dump([
|
183
|
+
body = MultiJson.dump({'node' => {'nodes' => [
|
183
184
|
{'action' => 'GET', 'key' => '/foo/bar', 'value' => 'bar', 'index' => 31},
|
184
185
|
{'action' => 'GET', 'key' => '/foo/baz', 'value' => 'baz', 'index' => 55},
|
185
|
-
])
|
186
|
+
]}})
|
186
187
|
stub_request(:get, "#{base_uri}/keys/foo").to_return(body: body)
|
187
188
|
info = client.info('/foo')
|
188
|
-
info
|
189
|
-
info['/foo/
|
190
|
-
info['/foo/
|
191
|
-
info['/foo/
|
192
|
-
info['/foo/
|
193
|
-
info['/foo/
|
189
|
+
puts info
|
190
|
+
info['/foo/bar'][:key].should eq('/foo/bar')
|
191
|
+
info['/foo/baz'][:key].should eq('/foo/baz')
|
192
|
+
info['/foo/bar'][:value].should eq('bar')
|
193
|
+
info['/foo/baz'][:value].should eq('baz')
|
194
|
+
info['/foo/bar'][:index].should eq(31)
|
195
|
+
info['/foo/baz'][:index].should eq(55)
|
194
196
|
end
|
195
197
|
end
|
196
198
|
end
|
@@ -211,16 +213,16 @@ module Etcd
|
|
211
213
|
with_stubbed_leaders(healthy_cluster_config)
|
212
214
|
|
213
215
|
client = Etcd::Client.connect(:uris => etcd1_uri)
|
214
|
-
client.leader.
|
215
|
-
client.leader.name.should
|
216
|
+
client.leader.client_urls.first.should eq(etcd1_uri)
|
217
|
+
client.leader.name.should eq("node1")
|
216
218
|
|
217
219
|
with_stubbed_leaders(healthy_cluster_changed_leader_config)
|
218
220
|
|
219
|
-
stub_request(:
|
220
|
-
stub_request(:
|
221
|
+
stub_request(:put, "#{etcd1_uri}/v2/keys/foo").to_return(status: 307, headers: {'Location' => "#{etcd2_uri}/v2/keys/foo"})
|
222
|
+
stub_request(:put, "#{etcd2_uri}/v2/keys/foo").to_return(body: MultiJson.dump({'value' => 'bar'}))
|
221
223
|
client.set("foo", "bar")
|
222
|
-
client.leader.
|
223
|
-
client.leader.name.should
|
224
|
+
client.leader.client_urls.first.should eq(etcd2_uri)
|
225
|
+
client.leader.name.should eq("node2")
|
224
226
|
end
|
225
227
|
end
|
226
228
|
|