etcdv3 0.6.0 → 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 30310caddc0edd306f6c292f980628a01e9555c6
4
- data.tar.gz: 602d665808709503255bb941b604482ca5a0b91e
3
+ metadata.gz: ee9b93afa5a82fab266b356a684bd201168046ae
4
+ data.tar.gz: 9b47ef6e8dd0d1caf32ec0b8453d55f771142e56
5
5
  SHA512:
6
- metadata.gz: 9d2dd95400f50f1a185593659d5651f9607b20fadd6f9bf712cb85e8a8832f735ef71a6b52dd7acaa76e46e43139a3ffa1031c8c5c34b10ef9dfd26e834eba7a
7
- data.tar.gz: a26775aba02b2b2a7f3244544362cf33cfcdfd748196d3fedee1721357dbf2b2dd73e985db6dd7424ca4c11af48588ecd9a14c07a4a90266bafaebbbac5d8794
6
+ metadata.gz: bf58b24446932842b4984452443fdc71b38610f8764d74875ef6bf6ef66fb8e85db268686c1546b4f8fe49414e19225b68477787177282a0ccf94cb1276a2c17
7
+ data.tar.gz: 4b1facb0dbc8a96bc1462a5053e96db38ffa781609048a19b742490f3dd0dc0e53983e2788164a96470c2ab3ea4a42fae469c698e134e89703f30cdd3c3fae0a
data/.travis.yml CHANGED
@@ -1,12 +1,11 @@
1
1
  language: ruby
2
2
  rvm:
3
- - 2.4.0
3
+ - 2.4.1
4
4
  - 2.2
5
- - 2.0
6
5
 
7
6
  env:
8
7
  global:
9
- - ETCD_VERSION=v3.1.6
8
+ - ETCD_VERSION=v3.2.0
10
9
 
11
10
  install:
12
11
  - bundle install
data/README.md CHANGED
@@ -3,8 +3,6 @@
3
3
 
4
4
  Ruby client for Etcd V3
5
5
 
6
- **Warning: This is under active development and should be considered unstable**
7
-
8
6
  ## Getting Started
9
7
 
10
8
  [RubyDocs](http://www.rubydoc.info/gems/etcdv3)
@@ -20,17 +18,22 @@ gem install etcdv3
20
18
  require 'etcdv3'
21
19
 
22
20
  # Insecure connection
23
- conn = Etcdv3.new(url: 'http://127.0.0.1:2379')
21
+ conn = Etcdv3.new(endpoints: 'http://127.0.0.1:2379, http://127.0.0.1:2389, http://127.0.0.1:2399')
24
22
 
25
23
  # Secure connection using default certificates
26
- conn = Etcdv3.new(url: 'https://hostname:port')
24
+ conn = Etcdv3.new(endpoints: 'https://hostname:port')
27
25
 
28
26
  # Secure connection with Auth
29
- conn = Etcdv3.new(url: 'https://hostname:port', user: 'root', password: 'mysecretpassword')
27
+ conn = Etcdv3.new(endpoints: 'https://hostname:port', user: 'root', password: 'mysecretpassword')
30
28
 
31
- # Secure connection specifying own certificates
29
+ # Secure connection specifying custom certificates
32
30
  # Coming soon...
31
+
33
32
  ```
33
+ **High Availability**
34
+
35
+ In the event of a failure, the client will work to restore connectivity by cycling through the specified endpoints until a connection can be established. With that being said, it is encouraged to specify multiple endpoints when available.
36
+
34
37
 
35
38
  ## Adding, Fetching and Deleting Keys
36
39
  ```ruby
@@ -122,7 +125,7 @@ Transactions provide an easy way to process multiple requests in a single transa
122
125
  _Note: You cannot modify the same key multiple times within a single transaction._
123
126
 
124
127
  ```ruby
125
- # https://github.com/davissp14/etcdv3-ruby/blob/txns/lib/etcdv3/kv/transaction.rb
128
+ # https://github.com/davissp14/etcdv3-ruby/blob/master/lib/etcdv3/kv/transaction.rb
126
129
  conn.transaction do |txn|
127
130
  txn.compare = [
128
131
  # Is the value of 'target_key' equal to 'compare_value'
@@ -130,13 +133,13 @@ conn.transaction do |txn|
130
133
  # Is the version of 'target_key' greater than 10
131
134
  txn.version('target_key', :greater, 10)
132
135
  ]
133
-
136
+
134
137
  txn.success = [
135
138
  txn.put('txn1', 'success')
136
139
  ]
137
-
140
+
138
141
  txn.failure = [
139
- txn.put('txn1', 'failed')
142
+ txn.put('txn1', 'failed', lease: lease_id)
140
143
  ]
141
144
  end
142
145
  ```
@@ -166,3 +169,12 @@ conn.alarm_list
166
169
  # Deactivate ALL active Alarms
167
170
  conn.alarm_deactivate
168
171
  ```
172
+
173
+ ```ruby
174
+ # Example
175
+ conn = Etcdv3.new(endpoints: 'http://127.0.0.1:2379, http://127.0.0.1:2389, http://127.0.0.1:2399')
176
+ ```
177
+
178
+ ## Contributing
179
+
180
+ If you're looking to get involved, [Fork the project](https://github.com/davissp14/etcdv3-ruby) and send pull requests.
data/etcdv3.gemspec CHANGED
@@ -14,7 +14,6 @@ Gem::Specification.new do |s|
14
14
  s.files = `git ls-files`.split("\n")
15
15
  s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
16
16
 
17
- s.add_dependency("grpc", "1.2.5")
18
- s.add_dependency("faraday", "0.11.0")
17
+ s.add_dependency("grpc", "1.6.0")
19
18
  s.add_development_dependency("rspec")
20
19
  end
@@ -0,0 +1,51 @@
1
+ class Etcdv3
2
+ class Connection
3
+
4
+ HANDLERS = {
5
+ auth: Etcdv3::Auth,
6
+ kv: Etcdv3::KV,
7
+ maintenance: Etcdv3::Maintenance,
8
+ lease: Etcdv3::Lease,
9
+ watch: Etcdv3::Watch
10
+ }
11
+
12
+ attr_reader :endpoint, :hostname, :handlers, :credentials
13
+
14
+ def initialize(url, metadata={})
15
+ @endpoint = URI(url)
16
+ @hostname = "#{@endpoint.hostname}:#{@endpoint.port}"
17
+ @credentials = resolve_credentials
18
+ @handlers = handler_map(metadata)
19
+ end
20
+
21
+ def call(stub, method, method_args=[])
22
+ @handlers.fetch(stub).send(method, *method_args)
23
+ end
24
+
25
+ def refresh_metadata(metadata)
26
+ @handlers = handler_map(metadata)
27
+ end
28
+
29
+ private
30
+
31
+ def handler_map(metadata={})
32
+ Hash[
33
+ HANDLERS.map do |key, klass|
34
+ [key, klass.new("#{@hostname}", @credentials, metadata)]
35
+ end
36
+ ]
37
+ end
38
+
39
+ def resolve_credentials
40
+ case @endpoint.scheme
41
+ when 'http'
42
+ :this_channel_is_insecure
43
+ when 'https'
44
+ # Use default certs for now.
45
+ GRPC::Core::ChannelCredentials.new
46
+ else
47
+ raise "Unknown scheme: #{@endpoint.scheme}"
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,55 @@
1
+ class Etcdv3
2
+ class ConnectionWrapper
3
+
4
+ attr_accessor :connection, :endpoints, :user, :password, :token
5
+
6
+ def initialize(endpoints)
7
+ @user, @password, @token = nil, nil, nil
8
+ @endpoints = endpoints.map{|endpoint| Etcdv3::Connection.new(endpoint) }
9
+ @connection = @endpoints.first
10
+ end
11
+
12
+ def handle(stub, method, method_args=[], retries: 1)
13
+ @connection.call(stub, method, method_args)
14
+
15
+ rescue GRPC::Unavailable, GRPC::Core::CallError => exception
16
+ $stderr.puts("Failed to connect to endpoint '#{@connection.hostname}'")
17
+ if @endpoints.size > 1
18
+ rotate_connection_endpoint
19
+ $stderr.puts("Failover event triggered. Failing over to '#{@connection.hostname}'")
20
+ return handle(stub, method, method_args)
21
+ else
22
+ return handle(stub, method, method_args)
23
+ end
24
+ rescue GRPC::Unauthenticated => exception
25
+ # Regenerate token in the event it expires.
26
+ if exception.details == 'etcdserver: invalid auth token'
27
+ if retries > 0
28
+ authenticate(@user, @password)
29
+ return handle(stub, method, method_args, retries: retries - 1)
30
+ end
31
+ end
32
+ raise exception
33
+ end
34
+
35
+ def clear_authentication
36
+ @user, @password, @token = nil, nil, nil
37
+ @connection.refresh_metadata({})
38
+ end
39
+
40
+ # Authenticate using specified user and password..
41
+ def authenticate(user, password)
42
+ @token = handle(:auth, 'generate_token', [user, password])
43
+ @user, @password = user, password
44
+ @connection.refresh_metadata(token: @token)
45
+ end
46
+
47
+ # Simple failover mechanism that rotates the connection endpoints in an
48
+ # attempt to recover connectivity.
49
+ def rotate_connection_endpoint
50
+ @endpoints.rotate!
51
+ @connection = @endpoints.first
52
+ @connection.refresh_metadata(token: @token) if @token
53
+ end
54
+ end
55
+ end
@@ -1,6 +1,20 @@
1
1
  class Etcdv3::KV
2
2
  module Requests
3
3
 
4
+ SORT_TARGET = {
5
+ key: 0,
6
+ version: 1,
7
+ create: 2,
8
+ mod: 3,
9
+ value: 4
10
+ }
11
+
12
+ SORT_ORDER = {
13
+ none: 0,
14
+ ascend: 1,
15
+ descend: 2
16
+ }
17
+
4
18
  def get_request(key, opts)
5
19
  opts[:sort_order] = SORT_ORDER[opts[:sort_order]] \
6
20
  if opts[:sort_order]
@@ -36,9 +36,9 @@ class Etcdv3::KV
36
36
 
37
37
  # Request Operations
38
38
 
39
- # txn.put('my', 'key')
40
- def put(key, value)
41
- put_request(key, value)
39
+ # txn.put('my', 'key', lease_id: 1)
40
+ def put(key, value, lease=nil)
41
+ put_request(key, value, lease)
42
42
  end
43
43
 
44
44
  # txn.get('key')
data/lib/etcdv3/kv.rb CHANGED
@@ -3,20 +3,6 @@ class Etcdv3
3
3
  class KV
4
4
  include Etcdv3::KV::Requests
5
5
 
6
- SORT_TARGET = {
7
- key: 0,
8
- version: 1,
9
- create: 2,
10
- mod: 3,
11
- value: 4
12
- }
13
-
14
- SORT_ORDER = {
15
- none: 0,
16
- ascend: 1,
17
- descend: 2
18
- }
19
-
20
6
  def initialize(hostname, credentials, metadata={})
21
7
  @stub = Etcdserverpb::KV::Stub.new(hostname, credentials)
22
8
  @metadata = metadata
@@ -1,3 +1,3 @@
1
1
  class Etcdv3
2
- VERSION = '0.6.0'.freeze
2
+ VERSION = '0.7.0'.freeze
3
3
  end
data/lib/etcdv3.rb CHANGED
@@ -1,4 +1,3 @@
1
-
2
1
  require 'grpc'
3
2
  require 'uri'
4
3
 
@@ -10,89 +9,58 @@ require 'etcdv3/kv'
10
9
  require 'etcdv3/maintenance'
11
10
  require 'etcdv3/lease'
12
11
  require 'etcdv3/watch'
13
-
14
- require 'etcdv3/request'
12
+ require 'etcdv3/connection'
13
+ require 'etcdv3/connection_wrapper'
15
14
 
16
15
  class Etcdv3
16
+ extend Forwardable
17
+ def_delegators :@conn, :user, :password, :token, :endpoints, :authenticate
17
18
 
18
- attr_reader :credentials, :options
19
-
20
- def uri
21
- URI(@options[:url])
22
- end
23
-
24
- def scheme
25
- uri.scheme
26
- end
27
-
28
- def port
29
- uri.port
30
- end
31
-
32
- def hostname
33
- uri.hostname
34
- end
35
-
36
- def user
37
- request.user
38
- end
39
-
40
- def password
41
- request.password
42
- end
43
-
44
- def token
45
- request.token
46
- end
19
+ attr_reader :conn, :options
47
20
 
48
21
  def initialize(options = {})
49
22
  @options = options
50
- @credentials = resolve_credentials
51
- authenticate(options[:user], options[:password]) unless options[:user].nil?
23
+ @conn = ConnectionWrapper.new(sanitized_endpoints)
24
+ warn "WARNING: `url` is deprecated. Please use `endpoints` instead." if @options.key?(:url)
25
+ authenticate(@options[:user], @options[:password]) if @options.key?(:user)
52
26
  end
53
27
 
54
28
  # Version of Etcd running on member
55
29
  def version
56
- request.handle(:maintenance, 'member_status').version
30
+ @conn.handle(:maintenance, 'member_status').version
57
31
  end
58
32
 
59
33
  # Store size in bytes.
60
34
  def db_size
61
- request.handle(:maintenance, 'member_status').dbSize
35
+ @conn.handle(:maintenance, 'member_status').dbSize
62
36
  end
63
37
 
64
38
  # Cluster leader id
65
39
  def leader_id
66
- request.handle(:maintenance, 'member_status').leader
40
+ @conn.handle(:maintenance, 'member_status').leader
67
41
  end
68
42
 
69
43
  # List active alarms
70
44
  def alarm_list
71
- request.handle(:maintenance, 'alarms', [:get, leader_id])
45
+ @conn.handle(:maintenance, 'alarms', [:get, leader_id])
72
46
  end
73
47
 
74
48
  # Disarm alarms on a specified member.
75
49
  def alarm_deactivate
76
- request.handle(:maintenance, 'alarms', [:deactivate, leader_id])
77
- end
78
-
79
- # Authenticate using specified user and password.
80
- # On successful authentication, an auth token will be assigned to the request instance.
81
- def authenticate(user, password)
82
- request.authenticate(user, password)
50
+ @conn.handle(:maintenance, 'alarms', [:deactivate, leader_id])
83
51
  end
84
52
 
85
53
  # Enables authentication.
86
54
  def auth_enable
87
- request.handle(:auth, 'auth_enable')
55
+ @conn.handle(:auth, 'auth_enable')
88
56
  true
89
57
  end
90
58
 
91
59
  # Disables authentication.
92
60
  # This will clear any active auth / token data.
93
61
  def auth_disable
94
- request.handle(:auth, 'auth_disable')
95
- request(reset: true)
62
+ @conn.handle(:auth, 'auth_disable')
63
+ @conn.clear_authentication
96
64
  true
97
65
  end
98
66
 
@@ -110,123 +78,110 @@ class Etcdv3
110
78
  # optional :min_create_revision - integer
111
79
  # optional :max_create_revision - integer
112
80
  def get(key, opts={})
113
- request.handle(:kv, 'get', [key, opts])
81
+ @conn.handle(:kv, 'get', [key, opts])
114
82
  end
115
83
 
116
84
  # Inserts a new key.
117
85
  def put(key, value, lease_id: nil)
118
- request.handle(:kv, 'put', [key, value, lease_id])
86
+ @conn.handle(:kv, 'put', [key, value, lease_id])
119
87
  end
120
88
 
121
89
  # Deletes a specified key
122
90
  def del(key, range_end: '')
123
- request.handle(:kv, 'del', [key, range_end])
91
+ @conn.handle(:kv, 'del', [key, range_end])
124
92
  end
125
93
 
126
94
  # Grant a lease with a specified TTL
127
95
  def lease_grant(ttl)
128
- request.handle(:lease, 'lease_grant', [ttl])
96
+ @conn.handle(:lease, 'lease_grant', [ttl])
129
97
  end
130
98
 
131
99
  # Revokes lease and delete all attached keys
132
100
  def lease_revoke(id)
133
- request.handle(:lease, 'lease_revoke', [id])
101
+ @conn.handle(:lease, 'lease_revoke', [id])
134
102
  end
135
103
 
136
104
  # Returns information regarding the current state of the lease
137
105
  def lease_ttl(id)
138
- request.handle(:lease, 'lease_ttl', [id])
106
+ @conn.handle(:lease, 'lease_ttl', [id])
139
107
  end
140
108
 
141
109
  # List all roles.
142
110
  def role_list
143
- request.handle(:auth, 'role_list')
111
+ @conn.handle(:auth, 'role_list')
144
112
  end
145
113
 
146
114
  # Add role with specified name.
147
115
  def role_add(name)
148
- request.handle(:auth, 'role_add', [name])
116
+ @conn.handle(:auth, 'role_add', [name])
149
117
  end
150
118
 
151
119
  # Fetches a specified role.
152
120
  def role_get(name)
153
- request.handle(:auth, 'role_get', [name])
121
+ @conn.handle(:auth, 'role_get', [name])
154
122
  end
155
123
 
156
124
  # Delete role.
157
125
  def role_delete(name)
158
- request.handle(:auth, 'role_delete', [name])
126
+ @conn.handle(:auth, 'role_delete', [name])
159
127
  end
160
128
 
161
129
  # Grants a new permission to an existing role.
162
130
  def role_grant_permission(name, permission, key, range_end='')
163
- request.handle(:auth, 'role_grant_permission', [name, permission, key, range_end])
131
+ @conn.handle(:auth, 'role_grant_permission', [name, permission, key, range_end])
164
132
  end
165
133
 
166
134
  def role_revoke_permission(name, permission, key, range_end='')
167
- request.handle(:auth, 'role_revoke_permission', [name, permission, key, range_end])
135
+ @conn.handle(:auth, 'role_revoke_permission', [name, permission, key, range_end])
168
136
  end
169
137
 
170
138
  # Fetch specified user
171
139
  def user_get(user)
172
- request.handle(:auth, 'user_get', [user])
140
+ @conn.handle(:auth, 'user_get', [user])
173
141
  end
174
142
 
175
143
  # Creates new user.
176
144
  def user_add(user, password)
177
- request.handle(:auth, 'user_add', [user, password])
145
+ @conn.handle(:auth, 'user_add', [user, password])
178
146
  end
179
147
 
180
148
  # Delete specified user.
181
149
  def user_delete(user)
182
- request.handle(:auth, 'user_delete', [user])
150
+ @conn.handle(:auth, 'user_delete', [user])
183
151
  end
184
152
 
185
153
  # Changes the specified users password.
186
154
  def user_change_password(user, new_password)
187
- request.handle(:auth, 'user_change_password', [user, new_password])
155
+ @conn.handle(:auth, 'user_change_password', [user, new_password])
188
156
  end
189
157
 
190
158
  # List all users.
191
159
  def user_list
192
- request.handle(:auth, 'user_list')
160
+ @conn.handle(:auth, 'user_list')
193
161
  end
194
162
 
195
163
  # Grants role to an existing user.
196
164
  def user_grant_role(user, role)
197
- request.handle(:auth, 'user_grant_role', [user, role])
165
+ @conn.handle(:auth, 'user_grant_role', [user, role])
198
166
  end
199
167
 
200
168
  # Revokes role from a specified user.
201
169
  def user_revoke_role(user, role)
202
- request.handle(:auth, 'user_revoke_role', [user, role])
170
+ @conn.handle(:auth, 'user_revoke_role', [user, role])
203
171
  end
204
172
 
205
173
  # Watches for changes on a specified key range.
206
174
  def watch(key, range_end: '', &block)
207
- request.handle(:watch, 'watch', [key, range_end, block])
175
+ @conn.handle(:watch, 'watch', [key, range_end, block])
208
176
  end
209
177
 
210
178
  def transaction(&block)
211
- request.handle(:kv, 'transaction', [block])
179
+ @conn.handle(:kv, 'transaction', [block])
212
180
  end
213
181
 
214
182
  private
215
183
 
216
- def request(reset: false)
217
- return @request if @request && !reset
218
- @request = Request.new("#{hostname}:#{port}", @credentials)
219
- end
220
-
221
- def resolve_credentials
222
- case scheme
223
- when 'http'
224
- :this_channel_is_insecure
225
- when 'https'
226
- # Use default certs for now.
227
- GRPC::Core::ChannelCredentials.new
228
- else
229
- raise "Unknown scheme: #{scheme}"
230
- end
184
+ def sanitized_endpoints
185
+ (@options[:endpoints] || @options[:url]).split(',').map(&:strip)
231
186
  end
232
187
  end
@@ -0,0 +1,49 @@
1
+ require 'spec_helper'
2
+
3
+ describe Etcdv3::Connection do
4
+
5
+ describe '#initialize - without metadata' do
6
+ subject { Etcdv3::Connection.new('http://localhost:2379') }
7
+
8
+ it { is_expected.to have_attributes(endpoint: URI('http://localhost:2379')) }
9
+ it { is_expected.to have_attributes(credentials: :this_channel_is_insecure) }
10
+ it { is_expected.to have_attributes(hostname: 'localhost:2379') }
11
+
12
+ [:kv, :maintenance, :lease, :watch, :auth].each do |handler|
13
+ let(:handler_stub) { subject.handlers[handler].instance_variable_get(:@stub) }
14
+ let(:handler_metadata) { subject.handlers[handler].instance_variable_get(:@metadata) }
15
+ it 'sets hostname' do
16
+ expect(handler_stub.instance_variable_get(:@host)).to eq('localhost:2379')
17
+ end
18
+ it 'sets token' do
19
+ expect(handler_metadata[:token]).to be_nil
20
+ end
21
+ end
22
+ end
23
+
24
+ describe '#initialize - with metadata' do
25
+ subject { Etcdv3::Connection.new('http://localhost:2379', token: 'token123') }
26
+
27
+ [:kv, :maintenance, :lease, :watch, :auth].each do |handler|
28
+ let(:handler_stub) { subject.handlers[handler].instance_variable_get(:@stub) }
29
+ let(:handler_metadata) { subject.handlers[handler].instance_variable_get(:@metadata) }
30
+ it 'sets hostname' do
31
+ expect(handler_stub.instance_variable_get(:@host)).to eq('localhost:2379')
32
+ end
33
+ it 'sets token' do
34
+ expect(handler_metadata[:token]).to eq('token123')
35
+ end
36
+ end
37
+ end
38
+
39
+ describe '#refresh_metadata' do
40
+ subject { Etcdv3::Connection.new('http://localhost:2379', token: 'token123') }
41
+ before { subject.refresh_metadata(token: 'newtoken') }
42
+ [:kv, :maintenance, :lease, :watch, :auth].each do |handler|
43
+ let(:handler_metadata) { subject.handlers[handler].instance_variable_get(:@metadata) }
44
+ it 'rebuilds handlers with new token' do
45
+ expect(handler_metadata[:token]).to eq('newtoken')
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,76 @@
1
+ require 'spec_helper'
2
+
3
+ describe Etcdv3::ConnectionWrapper do
4
+ let(:conn) { local_connection }
5
+ let(:endpoints) { ['http://localhost:2379', 'http://localhost:2389'] }
6
+
7
+ describe '#initialize' do
8
+ subject { Etcdv3::ConnectionWrapper.new(endpoints) }
9
+ it { is_expected.to have_attributes(user: nil, password: nil, token: nil) }
10
+ it 'sets hostnames in correct order' do
11
+ expect(subject.endpoints.map(&:hostname)).to eq(['localhost:2379', 'localhost:2389'])
12
+ end
13
+ it 'stubs connection with the correct hostname' do
14
+ expect(subject.connection.hostname).to eq('localhost:2379')
15
+ end
16
+ end
17
+
18
+ describe "#rotate_connection_endpoint" do
19
+ subject { Etcdv3::ConnectionWrapper.new(endpoints) }
20
+ before do
21
+ subject.rotate_connection_endpoint
22
+ end
23
+ it 'sets hostnames in correct order' do
24
+ expect(subject.endpoints.map(&:hostname)).to eq(['localhost:2389', 'localhost:2379'])
25
+ end
26
+ it 'sets correct hostname' do
27
+ expect(subject.connection.hostname).to eq('localhost:2389')
28
+ end
29
+ end
30
+
31
+ describe "Failover Simulation" do
32
+ let(:modified_conn) { local_connection("http://localhost:2369, http://localhost:2379") }
33
+ context 'without auth' do
34
+ # Set primary endpoint to a non-existing etcd endpoint
35
+ subject { modified_conn.get('boom') }
36
+ it { is_expected.to be_an_instance_of(Etcdserverpb::RangeResponse) }
37
+ end
38
+ context 'with auth' do
39
+ before do
40
+ # Establish connection with auth using real endpoint.
41
+ modified_conn.send(:conn).rotate_connection_endpoint
42
+ modified_conn.user_add('root', 'pass')
43
+ modified_conn.user_grant_role('root', 'root')
44
+ modified_conn.auth_enable
45
+ modified_conn.authenticate('root', 'pass')
46
+ # Rotate connections so we initiate connection using bad endpoint
47
+ modified_conn.send(:conn).rotate_connection_endpoint
48
+ end
49
+ after do
50
+ modified_conn.auth_disable
51
+ modified_conn.user_delete('root')
52
+ end
53
+ subject { modified_conn.get('boom') }
54
+ it { is_expected.to be_an_instance_of(Etcdserverpb::RangeResponse) }
55
+ end
56
+ end
57
+
58
+ describe "GRPC::Unauthenticated recovery" do
59
+ let(:wrapper) { conn.send(:conn) }
60
+ let(:connection) { wrapper.connection }
61
+ before do
62
+ conn.user_add('root', 'pass')
63
+ conn.user_grant_role('root', 'root')
64
+ conn.auth_enable
65
+ conn.authenticate('root', 'pass')
66
+ wrapper.token = "thiswontwork"
67
+ connection.refresh_metadata(token: "thiswontwork")
68
+ end
69
+ after do
70
+ conn.auth_disable
71
+ conn.user_delete('root')
72
+ end
73
+ subject { conn.user_get('root') }
74
+ it { is_expected.to be_an_instance_of(Etcdserverpb::AuthUserGetResponse) }
75
+ end
76
+ end
data/spec/etcdv3_spec.rb CHANGED
@@ -8,9 +8,6 @@ describe Etcdv3 do
8
8
  describe '#initialize' do
9
9
  context 'without auth' do
10
10
  subject { conn }
11
- it { is_expected.to have_attributes(scheme: 'http') }
12
- it { is_expected.to have_attributes(hostname: '127.0.0.1') }
13
- it { is_expected.to have_attributes(credentials: :this_channel_is_insecure) }
14
11
  it { is_expected.to have_attributes(token: nil) }
15
12
  it { is_expected.to have_attributes(user: nil) }
16
13
  it { is_expected.to have_attributes(password: nil) }
@@ -238,53 +235,149 @@ describe Etcdv3 do
238
235
  end
239
236
 
240
237
  context 'auth disabled' do
238
+ before do
239
+ conn.user_add('root', 'root')
240
+ conn.auth_disable
241
+ end
242
+ after do
243
+ conn.user_delete('root')
244
+ end
241
245
  it 'raises error' do
242
- expect { conn.authenticate('root', 'root') }.to raise_error(GRPC::InvalidArgument)
246
+ expect { conn.authenticate('root', 'root') }.to raise_error(GRPC::FailedPrecondition)
243
247
  end
244
248
  end
245
249
  end
246
250
 
247
251
  describe '#transaction' do
248
- context 'success' do
252
+ describe 'txn.value' do
249
253
  before { conn.put('txn', 'value') }
250
254
  after { conn.del('txn') }
251
- subject do
252
- conn.transaction do |txn|
253
- txn.compare = [ txn.value('txn', :equal, 'value') ]
254
- txn.success = [ txn.put('txn-test', 'success') ]
255
- txn.failure = [ txn.put('txn-test', 'failed') ]
255
+ context 'success' do
256
+ subject! do
257
+ conn.transaction do |txn|
258
+ txn.compare = [ txn.value('txn', :equal, 'value') ]
259
+ txn.success = [ txn.put('txn-test', 'success') ]
260
+ txn.failure = [ txn.put('txn-test', 'failed') ]
261
+ end
262
+ end
263
+ it 'sets correct key' do
264
+ expect(conn.get('txn-test').kvs.first.value).to eq('success')
256
265
  end
257
266
  end
258
- it { is_expected.to be_an_instance_of(Etcdserverpb::TxnResponse) }
259
- it 'returns successful' do
260
- expect(subject.succeeded).to eq(true)
267
+ context "success, value with lease" do
268
+ let!(:lease_id) { conn.lease_grant(2)['ID'] }
269
+ subject! do
270
+ conn.transaction do |txn|
271
+ txn.compare = [ txn.value('txn', :equal, 'value') ]
272
+ txn.success = [ txn.put('txn-test', 'success', lease_id) ]
273
+ txn.failure = [ txn.put('txn-test', 'failed', lease_id) ]
274
+ end
275
+ end
276
+ it 'sets correct key, with a lease' do
277
+ expect(conn.get('txn-test').kvs.first.value).to eq('success')
278
+ expect(conn.get('txn-test').kvs.first.lease).to eq(lease_id)
279
+ end
261
280
  end
262
- it 'sets correct key' do
263
- expect(conn.get('txn-test').kvs.first.value).to eq('success')
281
+ context 'failure' do
282
+ subject! do
283
+ conn.transaction do |txn|
284
+ txn.compare = [ txn.value('txn', :equal, 'notright') ]
285
+ txn.success = [ txn.put('txn-test', 'success') ]
286
+ txn.failure = [ txn.put('txn-test', 'failed') ]
287
+ end
288
+ end
289
+ it 'sets correct key' do
290
+ expect(conn.get('txn-test').kvs.first.value).to eq('failed')
291
+ end
264
292
  end
265
293
  end
266
294
 
267
- context 'failure' do
295
+ describe 'txn.create_revision' do
268
296
  before { conn.put('txn', 'value') }
269
297
  after { conn.del('txn') }
270
- subject do
271
- conn.transaction do |txn|
272
- txn.compare = [
273
- txn.create_revision('txn', :greater, 500),
274
- txn.mod_revision('txn', :less, 1000)
275
- ]
276
- txn.success = [ txn.put('txn-test', 'success') ]
277
- txn.failure = [ txn.put('txn-test', 'failed') ]
298
+ context 'success' do
299
+ subject! do
300
+ conn.transaction do |txn|
301
+ txn.compare = [ txn.create_revision('txn', :greater, 1) ]
302
+ txn.success = [ txn.put('txn-test', 'success') ]
303
+ txn.failure = [ txn.put('txn-test', 'failed') ]
304
+ end
305
+ end
306
+ it 'sets correct key' do
307
+ expect(conn.get('txn-test').kvs.first.value).to eq('success')
308
+ end
309
+ end
310
+ context 'failure' do
311
+ subject! do
312
+ conn.transaction do |txn|
313
+ txn.compare = [ txn.create_revision('txn', :equal, 1) ]
314
+ txn.success = [ txn.put('txn-test', 'success') ]
315
+ txn.failure = [ txn.put('txn-test', 'failed') ]
316
+ end
317
+ end
318
+ it 'sets correct key' do
319
+ expect(conn.get('txn-test').kvs.first.value).to eq('failed')
320
+ end
321
+ end
322
+ end
323
+
324
+ describe 'txn.mod_revision' do
325
+ before { conn.put('txn', 'value') }
326
+ after { conn.del('txn') }
327
+ context 'success' do
328
+ subject! do
329
+ conn.transaction do |txn|
330
+ txn.compare = [ txn.mod_revision('txn', :less, 1000) ]
331
+ txn.success = [ txn.put('txn-test', 'success') ]
332
+ txn.failure = [ txn.put('txn-test', 'failed') ]
333
+ end
334
+ end
335
+ it 'sets correct key' do
336
+ expect(conn.get('txn-test').kvs.first.value).to eq('success')
337
+ end
338
+ end
339
+ context 'failure' do
340
+ subject! do
341
+ conn.transaction do |txn|
342
+ txn.compare = [ txn.mod_revision('txn', :greater, 1000) ]
343
+ txn.success = [ txn.put('txn-test', 'success') ]
344
+ txn.failure = [ txn.put('txn-test', 'failed') ]
345
+ end
346
+ end
347
+ it 'sets correct key' do
348
+ expect(conn.get('txn-test').kvs.first.value).to eq('failed')
278
349
  end
279
350
  end
280
- it 'returns successful' do
281
- expect(subject.succeeded).to eq(false)
351
+ end
352
+
353
+ describe 'txn.version' do
354
+ before { conn.put('txn', 'value') }
355
+ after { conn.del('txn') }
356
+ context 'success' do
357
+ subject! do
358
+ conn.transaction do |txn|
359
+ txn.compare = [ txn.version('txn', :equal, 1) ]
360
+ txn.success = [ txn.put('txn-test', 'success') ]
361
+ txn.failure = [ txn.put('txn-test', 'failed') ]
362
+ end
363
+ end
364
+ it 'sets correct key' do
365
+ expect(conn.get('txn-test').kvs.first.value).to eq('success')
366
+ end
282
367
  end
283
- it 'sets correct key' do
284
- expect(conn.get('txn-test').kvs.first.value).to eq('failed')
368
+ context 'failure' do
369
+ subject! do
370
+ conn.transaction do |txn|
371
+ txn.compare = [ txn.version('txn', :equal, 100)]
372
+ txn.success = [ txn.put('txn-test', 'success') ]
373
+ txn.failure = [ txn.put('txn-test', 'failed') ]
374
+ end
375
+ end
376
+ it 'sets correct key' do
377
+ expect(conn.get('txn-test').kvs.first.value).to eq('failed')
378
+ end
285
379
  end
286
380
  end
287
381
  end
288
-
289
382
  end
290
383
  end
@@ -2,11 +2,11 @@ module Helpers
2
2
  module Connections
3
3
 
4
4
  def local_connection_with_auth(user, password)
5
- Etcdv3.new(url: "http://#{local_url}", user: user, password: password)
5
+ Etcdv3.new(endpoints: "http://#{local_url}", user: user, password: password)
6
6
  end
7
7
 
8
- def local_connection
9
- Etcdv3.new(url: "http://#{local_url}")
8
+ def local_connection(endpoints="http://#{local_url}")
9
+ Etcdv3.new(endpoints: endpoints)
10
10
  end
11
11
 
12
12
  def local_stub(interface)
@@ -17,6 +17,10 @@ module Helpers
17
17
  "127.0.0.1:#{port}"
18
18
  end
19
19
 
20
+ def full_local_url
21
+ "http://#{local_url}"
22
+ end
23
+
20
24
  def port
21
25
  ENV.fetch('ETCD_TEST_PORT', 2379).to_i
22
26
  end
data/spec/spec_helper.rb CHANGED
@@ -23,6 +23,7 @@ RSpec.configure do |config|
23
23
 
24
24
  instance = Helpers::TestInstance.new
25
25
  config.before(:suite) do
26
+ # $stderr = File.open(File::NULL, "w")
26
27
  instance.start
27
28
  end
28
29
  config.after(:suite) do
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: etcdv3
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.0
4
+ version: 0.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Shaun Davis
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2017-04-21 00:00:00.000000000 Z
11
+ date: 2017-09-17 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: grpc
@@ -16,28 +16,14 @@ dependencies:
16
16
  requirements:
17
17
  - - '='
18
18
  - !ruby/object:Gem::Version
19
- version: 1.2.5
19
+ version: 1.6.0
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - '='
25
25
  - !ruby/object:Gem::Version
26
- version: 1.2.5
27
- - !ruby/object:Gem::Dependency
28
- name: faraday
29
- requirement: !ruby/object:Gem::Requirement
30
- requirements:
31
- - - '='
32
- - !ruby/object:Gem::Version
33
- version: 0.11.0
34
- type: :runtime
35
- prerelease: false
36
- version_requirements: !ruby/object:Gem::Requirement
37
- requirements:
38
- - - '='
39
- - !ruby/object:Gem::Version
40
- version: 0.11.0
26
+ version: 1.6.0
41
27
  - !ruby/object:Gem::Dependency
42
28
  name: rspec
43
29
  requirement: !ruby/object:Gem::Requirement
@@ -67,6 +53,8 @@ files:
67
53
  - etcdv3.gemspec
68
54
  - lib/etcdv3.rb
69
55
  - lib/etcdv3/auth.rb
56
+ - lib/etcdv3/connection.rb
57
+ - lib/etcdv3/connection_wrapper.rb
70
58
  - lib/etcdv3/etcdrpc/auth_pb.rb
71
59
  - lib/etcdv3/etcdrpc/kv_pb.rb
72
60
  - lib/etcdv3/etcdrpc/rpc_pb.rb
@@ -83,10 +71,11 @@ files:
83
71
  - lib/etcdv3/protos/http.proto
84
72
  - lib/etcdv3/protos/kv.proto
85
73
  - lib/etcdv3/protos/rpc.proto
86
- - lib/etcdv3/request.rb
87
74
  - lib/etcdv3/version.rb
88
75
  - lib/etcdv3/watch.rb
89
76
  - spec/etcdv3/auth_spec.rb
77
+ - spec/etcdv3/connection_spec.rb
78
+ - spec/etcdv3/connection_wrapper_spec.rb
90
79
  - spec/etcdv3/kv_spec.rb
91
80
  - spec/etcdv3/lease_spec.rb
92
81
  - spec/etcdv3/maintenance_spec.rb
@@ -120,6 +109,8 @@ specification_version: 4
120
109
  summary: A Etcd client library for Version 3
121
110
  test_files:
122
111
  - spec/etcdv3/auth_spec.rb
112
+ - spec/etcdv3/connection_spec.rb
113
+ - spec/etcdv3/connection_wrapper_spec.rb
123
114
  - spec/etcdv3/kv_spec.rb
124
115
  - spec/etcdv3/lease_spec.rb
125
116
  - spec/etcdv3/maintenance_spec.rb
@@ -1,53 +0,0 @@
1
- require 'base64'
2
- class Etcdv3
3
- class Request
4
-
5
- HANDLERS = {
6
- auth: Etcdv3::Auth,
7
- kv: Etcdv3::KV,
8
- maintenance: Etcdv3::Maintenance,
9
- lease: Etcdv3::Lease,
10
- watch: Etcdv3::Watch
11
- }
12
-
13
- attr_reader :user, :password, :token
14
-
15
- def initialize(hostname, credentials)
16
- @user, @password, @token = nil, nil, nil
17
- @hostname = hostname
18
- @credentials = credentials
19
- @handlers = handler_map
20
- end
21
-
22
- def handle(stub, method, method_args=[], retries: 1)
23
- @handlers.fetch(stub).send(method, *method_args)
24
- rescue GRPC::Unauthenticated => exception
25
- # Regenerate token in the event it expires.
26
- if exception.details == 'etcdserver: invalid auth token'
27
- if retries > 0
28
- authenticate(@user, @password)
29
- return handle(stub, method, method_args, retries: retries - 1)
30
- end
31
- end
32
- raise exception
33
- end
34
-
35
- def authenticate(user, password)
36
- # Attempt to generate token using user and password.
37
- @token = handle(:auth, 'generate_token', [user, password])
38
- @user = user
39
- @password = password
40
- @handlers = handler_map(token: @token)
41
- end
42
-
43
- private
44
-
45
- def handler_map(metadata={})
46
- Hash[
47
- HANDLERS.map do |key, klass|
48
- [key, klass.new(@hostname, @credentials, metadata)]
49
- end
50
- ]
51
- end
52
- end
53
- end