etcdv3 0.6.0 → 0.7.0

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