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