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 +4 -4
- data/.travis.yml +2 -3
- data/README.md +22 -10
- data/etcdv3.gemspec +1 -2
- data/lib/etcdv3/connection.rb +51 -0
- data/lib/etcdv3/connection_wrapper.rb +55 -0
- data/lib/etcdv3/kv/requests.rb +14 -0
- data/lib/etcdv3/kv/transaction.rb +3 -3
- data/lib/etcdv3/kv.rb +0 -14
- data/lib/etcdv3/version.rb +1 -1
- data/lib/etcdv3.rb +39 -84
- data/spec/etcdv3/connection_spec.rb +49 -0
- data/spec/etcdv3/connection_wrapper_spec.rb +76 -0
- data/spec/etcdv3_spec.rb +122 -29
- data/spec/helpers/connections.rb +7 -3
- data/spec/spec_helper.rb +1 -0
- metadata +10 -19
- data/lib/etcdv3/request.rb +0 -53
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ee9b93afa5a82fab266b356a684bd201168046ae
|
4
|
+
data.tar.gz: 9b47ef6e8dd0d1caf32ec0b8453d55f771142e56
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: bf58b24446932842b4984452443fdc71b38610f8764d74875ef6bf6ef66fb8e85db268686c1546b4f8fe49414e19225b68477787177282a0ccf94cb1276a2c17
|
7
|
+
data.tar.gz: 4b1facb0dbc8a96bc1462a5053e96db38ffa781609048a19b742490f3dd0dc0e53983e2788164a96470c2ab3ea4a42fae469c698e134e89703f30cdd3c3fae0a
|
data/.travis.yml
CHANGED
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(
|
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(
|
24
|
+
conn = Etcdv3.new(endpoints: 'https://hostname:port')
|
27
25
|
|
28
26
|
# Secure connection with Auth
|
29
|
-
conn = Etcdv3.new(
|
27
|
+
conn = Etcdv3.new(endpoints: 'https://hostname:port', user: 'root', password: 'mysecretpassword')
|
30
28
|
|
31
|
-
# Secure connection specifying
|
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/
|
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.
|
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
|
data/lib/etcdv3/kv/requests.rb
CHANGED
@@ -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
|
data/lib/etcdv3/version.rb
CHANGED
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/
|
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 :
|
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
|
-
@
|
51
|
-
|
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
|
-
|
30
|
+
@conn.handle(:maintenance, 'member_status').version
|
57
31
|
end
|
58
32
|
|
59
33
|
# Store size in bytes.
|
60
34
|
def db_size
|
61
|
-
|
35
|
+
@conn.handle(:maintenance, 'member_status').dbSize
|
62
36
|
end
|
63
37
|
|
64
38
|
# Cluster leader id
|
65
39
|
def leader_id
|
66
|
-
|
40
|
+
@conn.handle(:maintenance, 'member_status').leader
|
67
41
|
end
|
68
42
|
|
69
43
|
# List active alarms
|
70
44
|
def alarm_list
|
71
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
95
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
106
|
+
@conn.handle(:lease, 'lease_ttl', [id])
|
139
107
|
end
|
140
108
|
|
141
109
|
# List all roles.
|
142
110
|
def role_list
|
143
|
-
|
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
|
-
|
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
|
-
|
121
|
+
@conn.handle(:auth, 'role_get', [name])
|
154
122
|
end
|
155
123
|
|
156
124
|
# Delete role.
|
157
125
|
def role_delete(name)
|
158
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
175
|
+
@conn.handle(:watch, 'watch', [key, range_end, block])
|
208
176
|
end
|
209
177
|
|
210
178
|
def transaction(&block)
|
211
|
-
|
179
|
+
@conn.handle(:kv, 'transaction', [block])
|
212
180
|
end
|
213
181
|
|
214
182
|
private
|
215
183
|
|
216
|
-
def
|
217
|
-
|
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::
|
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
|
-
|
252
|
+
describe 'txn.value' do
|
249
253
|
before { conn.put('txn', 'value') }
|
250
254
|
after { conn.del('txn') }
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
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
|
-
|
259
|
-
|
260
|
-
|
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
|
-
|
263
|
-
|
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
|
-
|
295
|
+
describe 'txn.create_revision' do
|
268
296
|
before { conn.put('txn', 'value') }
|
269
297
|
after { conn.del('txn') }
|
270
|
-
|
271
|
-
|
272
|
-
|
273
|
-
txn.create_revision('txn', :greater,
|
274
|
-
txn.
|
275
|
-
|
276
|
-
|
277
|
-
|
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
|
-
|
281
|
-
|
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
|
-
|
284
|
-
|
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
|
data/spec/helpers/connections.rb
CHANGED
@@ -2,11 +2,11 @@ module Helpers
|
|
2
2
|
module Connections
|
3
3
|
|
4
4
|
def local_connection_with_auth(user, password)
|
5
|
-
Etcdv3.new(
|
5
|
+
Etcdv3.new(endpoints: "http://#{local_url}", user: user, password: password)
|
6
6
|
end
|
7
7
|
|
8
|
-
def local_connection
|
9
|
-
Etcdv3.new(
|
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
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.
|
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-
|
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.
|
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.
|
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
|
data/lib/etcdv3/request.rb
DELETED
@@ -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
|