etcdv3 0.10.2 → 0.11.3

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: db0142aa621e3ec4612d3d2051b06f208382001d
4
- data.tar.gz: a1d300e69e53c41a9a3864d6d480011cc7ffc6a1
2
+ SHA256:
3
+ metadata.gz: 59585a18eada27829edbf162733146e90e4c336bcbbefa761b3063e3f9400139
4
+ data.tar.gz: edc026da35b1396e664a431a79eb2012238ea4bd49c8ef43e1f70f93366935fe
5
5
  SHA512:
6
- metadata.gz: 8cd28535b2f39b4b04d0d426b1bbf940997e512922ddc5da85a86d69cf0c24319b40a187d1cde576c5a01ad33f9ee470a33ea02f9fa2c442f6aaee0484b61d36
7
- data.tar.gz: 3bb79400389240448d945e9b41b967cf4c4f65d1454bebdcaff4d8c60cbc82023b950e707f70c6bb87d89b0e3e13546eb64a05f1adcdf7356b0a75a35e0d3241
6
+ metadata.gz: d5afcdcad7f96e422bebcd6d4d4db148bbe48c8600be017650ae229d988cd2e5c6d3547f2026eb1db18a10c91643ba3e84bb0a2555b2f8add7f687dd9bfc014a
7
+ data.tar.gz: 310680f7f8f11b65dfb9ea1a116536394678faf05f29087a77b87b1665138f85c4bc39614299dff22a528a63f0a0771dbb3841e9b60a610611dacff33048fbf1
data/README.md CHANGED
@@ -26,14 +26,16 @@ conn = Etcdv3.new(endpoints: 'https://hostname:port')
26
26
  # Secure connection with Auth
27
27
  conn = Etcdv3.new(endpoints: 'https://hostname:port', user: 'root', password: 'mysecretpassword')
28
28
 
29
+ # Scope CRUD operations to a specific keyspace.
30
+ conn = Etcdv3.new(endpoints: 'https://hostname:port', namespace: "/target_keyspace/")
31
+
29
32
  # Secure connection specifying custom certificates
30
33
  # Coming soon...
31
34
 
32
35
  ```
33
36
  **High Availability**
34
37
 
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. That's the default
36
- behaviour.
38
+ 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.
37
39
 
38
40
  However, sometimes this is not what you want. If you need more control over
39
41
  failures, you can suppress this mechanism by using
@@ -43,12 +45,29 @@ conn = Etcdv3.new(endpoints: 'https://hostname:port', allow_reconnect: false)
43
45
  ```
44
46
 
45
47
  This will still rotate the endpoints, but it will raise an exception so you can
46
- handle the failure yourself. On next call new endpoint (since they were
47
- rotated) is tried. One thing you need to keep in mind if you are using etcd with
48
- authorization is that you need to take care of `GRPC::Unauthenticated` exceptions
49
- and manually re-authenticate when token expires. To reiterate, you are
50
- responsible for handling the errors, so some understanding of how this gem and
51
- etcd works is recommended.
48
+ handle the failure yourself. On next call to the new endpoint (since they were
49
+ rotated) is tried. One thing you need to keep in mind if auth is enabled, you
50
+ need to take care of `GRPC::Unauthenticated` exception and manually re-authenticate
51
+ when token expires. To reiterate, you are responsible for handling the errors, so
52
+ some understanding of how this gem and etcd works is recommended.
53
+
54
+ ## Namespace support
55
+
56
+ Namespacing is a convenience feature used to scope CRUD based operations to a specific keyspace.
57
+
58
+ ```ruby
59
+ # Establish connection
60
+ conn = Etcdv3.new(endpoints: 'https://hostname:port', namespace: '/service-a/')
61
+
62
+ # Write key to /service-a/test_key
63
+ conn.put("test_key", "value").
64
+
65
+ # Get the key we just wrote.
66
+ conn.get("test_key")
67
+ ```
68
+
69
+ _Note: Namespaces are stripped from responses._
70
+
52
71
 
53
72
  ## Adding, Fetching and Deleting Keys
54
73
  ```ruby
@@ -1,6 +1,12 @@
1
1
  class Etcdv3
2
2
  class Connection
3
3
 
4
+ NAMESPACE_HANDLERS = {
5
+ kv: Etcdv3::Namespace::KV,
6
+ watch: Etcdv3::Namespace::Watch,
7
+ lock: Etcdv3::Namespace::Lock,
8
+ }
9
+
4
10
  HANDLERS = {
5
11
  auth: Etcdv3::Auth,
6
12
  kv: Etcdv3::KV,
@@ -10,11 +16,12 @@ class Etcdv3
10
16
  lock: Etcdv3::Lock,
11
17
  }
12
18
 
13
- attr_reader :endpoint, :hostname, :handlers, :credentials
19
+ attr_reader :endpoint, :hostname, :handlers, :credentials, :namespace
14
20
 
15
- def initialize(url, timeout, metadata={})
21
+ def initialize(url, timeout, namespace, metadata={})
16
22
  @endpoint = URI(url)
17
23
  @hostname = "#{@endpoint.hostname}:#{@endpoint.port}"
24
+ @namespace = namespace
18
25
  @credentials = resolve_credentials
19
26
  @timeout = timeout
20
27
  @handlers = handler_map(metadata)
@@ -31,11 +38,19 @@ class Etcdv3
31
38
  private
32
39
 
33
40
  def handler_map(metadata={})
34
- Hash[
41
+ handlers = Hash[
35
42
  HANDLERS.map do |key, klass|
36
- [key, klass.new("#{@hostname}", @credentials, @timeout, metadata)]
43
+ [key, klass.new(@hostname, @credentials, @timeout, metadata)]
37
44
  end
38
45
  ]
46
+ # Override any handlers that are namespace compatable.
47
+ if @namespace
48
+ NAMESPACE_HANDLERS.each do |key, klass|
49
+ handlers[key] = klass.new(@hostname, @credentials, @timeout, @namespace, metadata)
50
+ end
51
+ end
52
+
53
+ handlers
39
54
  end
40
55
 
41
56
  def resolve_credentials
@@ -3,12 +3,13 @@ class Etcdv3
3
3
 
4
4
  attr_accessor :connection, :endpoints, :user, :password, :token, :timeout
5
5
 
6
- def initialize(timeout, *endpoints, allow_reconnect)
6
+ def initialize(timeout, *endpoints, namespace, allow_reconnect)
7
7
  @user, @password, @token = nil, nil, nil
8
8
  @timeout = timeout
9
- @endpoints = endpoints.map{|endpoint| Etcdv3::Connection.new(endpoint, @timeout) }
10
- @connection = @endpoints.first
9
+ @namespace = namespace
10
+ @endpoints = endpoints.map{|endpoint| Etcdv3::Connection.new(endpoint, @timeout, @namespace) }
11
11
  @allow_reconnect = allow_reconnect
12
+ @connection = @endpoints.first
12
13
  end
13
14
 
14
15
  private def retry_or_raise(*args)
@@ -26,7 +27,6 @@ class Etcdv3
26
27
  $stderr.puts("Failed to connect to endpoint '#{@connection.hostname}'")
27
28
  if @endpoints.size > 1
28
29
  rotate_connection_endpoint
29
- $stderr.puts("Failover event triggered. Failing over to '#{@connection.hostname}'")
30
30
  return retry_or_raise(stub, method, method_args)
31
31
  else
32
32
  return retry_or_raise(stub, method, method_args)
@@ -0,0 +1,54 @@
1
+ class Etcdv3::Namespace::KV
2
+ module Requests
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
+
18
+ def get_request(key, opts)
19
+ key = prepend_prefix(@namespace, key)
20
+ # In order to enforce the scope of the specified namespace, we are going to
21
+ # intercept the zero-byte reference and re-target everything under the given namespace.
22
+ if opts[:range_end] =~ /\x00/
23
+ opts[:range_end] = (@namespace[0..-2] + (@namespace[-1].ord + 1).chr)
24
+ else
25
+ opts[:range_end] = prepend_prefix(@namespace, opts[:range_end]) if opts[:range_end]
26
+ end
27
+ opts[:sort_order] = SORT_ORDER[opts[:sort_order]] \
28
+ if opts[:sort_order]
29
+ opts[:sort_target] = SORT_TARGET[opts[:sort_target]] \
30
+ if opts[:sort_target]
31
+ opts[:key] = key
32
+ Etcdserverpb::RangeRequest.new(opts)
33
+ end
34
+
35
+ def del_request(key, range_end=nil)
36
+ key = prepend_prefix(@namespace, key)
37
+ # In order to enforce the scope of the specified namespace, we are going to
38
+ # intercept the zero-byte reference and re-target everything under the given namespace.
39
+ if range_end =~ /\x00/
40
+ range_end = (@namespace[0..-2] + (@namespace[-1].ord + 1).chr)
41
+ else
42
+ range_end = prepend_prefix(@namespace, range_end) if range_end
43
+ end
44
+ Etcdserverpb::DeleteRangeRequest.new(key: key, range_end: range_end)
45
+ end
46
+
47
+ def put_request(key, value, lease=nil)
48
+ key = prepend_prefix(@namespace, key)
49
+ kv = Etcdserverpb::PutRequest.new(key: key, value: value)
50
+ kv.lease = lease if lease
51
+ kv
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,91 @@
1
+ class Etcdv3::Namespace::KV
2
+ class Transaction
3
+ include Etcdv3::Namespace::Utilities
4
+ include Etcdv3::Namespace::KV::Requests
5
+
6
+ # Available comparison identifiers.
7
+ COMPARISON_IDENTIFIERS = {
8
+ equal: 0,
9
+ greater: 1,
10
+ less: 2,
11
+ not_equal: 3
12
+ }
13
+
14
+ # Available targets to compare with.
15
+ TARGETS = {
16
+ version: 0,
17
+ create_revision: 1,
18
+ mod_revision: 2,
19
+ value: 3
20
+ }
21
+
22
+ attr_writer :compare, :success, :failure
23
+
24
+ def initialize(namespace)
25
+ @namespace = namespace
26
+ end
27
+
28
+ def compare
29
+ @compare ||= []
30
+ end
31
+
32
+ def success
33
+ @success ||= []
34
+ end
35
+
36
+ def failure
37
+ @failure ||=[]
38
+ end
39
+
40
+ # Request Operations
41
+
42
+ # txn.put('my', 'key', lease_id: 1)
43
+ def put(key, value, lease=nil)
44
+ put_request(key, value, lease)
45
+ end
46
+
47
+ # txn.get('key')
48
+ def get(key, opts={})
49
+ get_request(key, opts)
50
+ end
51
+
52
+ # txn.del('key')
53
+ def del(key, range_end='')
54
+ del_request(key, range_end)
55
+ end
56
+
57
+ ### Compare Operations
58
+
59
+ # txn.version('names', :greater, 0 )
60
+ def version(key, compare_type, value)
61
+ generate_compare(:version, key, compare_type, value)
62
+ end
63
+
64
+ # txn.value('names', :equal, 'wowza' )
65
+ def value(key, compare_type, value)
66
+ generate_compare(:value, key, compare_type, value)
67
+ end
68
+
69
+ # txn.mod_revision('names', :not_equal, 0)
70
+ def mod_revision(key, compare_type, value)
71
+ generate_compare(:mod_revision, key, compare_type, value)
72
+ end
73
+
74
+ # txn.create_revision('names', :less, 10)
75
+ def create_revision(key, compare_type, value)
76
+ generate_compare(:create_revision, key, compare_type, value)
77
+ end
78
+
79
+ private
80
+
81
+ def generate_compare(target_union, key, compare_type, value)
82
+ key = prepend_prefix(@namespace, key)
83
+ Etcdserverpb::Compare.new(
84
+ key: key,
85
+ result: COMPARISON_IDENTIFIERS[compare_type],
86
+ target: TARGETS[target_union],
87
+ target_union => value
88
+ )
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,62 @@
1
+ class Etcdv3::Namespace
2
+ class KV
3
+ include Etcdv3::Namespace::KV::Requests
4
+ include Etcdv3::Namespace::Utilities
5
+ include GRPC::Core::TimeConsts
6
+
7
+ def initialize(hostname, credentials, timeout, namespace, metadata={})
8
+ @stub = Etcdserverpb::KV::Stub.new(hostname, credentials)
9
+ @timeout = timeout
10
+ @namespace = namespace
11
+ @metadata = metadata
12
+ end
13
+
14
+ def get(key, opts={})
15
+ timeout = opts.delete(:timeout)
16
+ resp = @stub.range(get_request(key, opts), metadata: @metadata, deadline: deadline(timeout))
17
+ strip_prefix(@namespace, resp)
18
+ end
19
+
20
+ def del(key, range_end: '', timeout: nil)
21
+ resp = @stub.delete_range(del_request(key, range_end), metadata: @metadata, deadline: deadline(timeout))
22
+ strip_prefix(@namespace, resp)
23
+ end
24
+
25
+ def put(key, value, lease: nil, timeout: nil)
26
+ resp = @stub.put(put_request(key, value, lease), metadata: @metadata, deadline: deadline(timeout))
27
+ strip_prefix(@namespace, resp)
28
+ end
29
+
30
+ def transaction(block, timeout: nil)
31
+ txn = Etcdv3::Namespace::KV::Transaction.new(@namespace)
32
+ block.call(txn)
33
+ request = Etcdserverpb::TxnRequest.new(
34
+ compare: txn.compare,
35
+ success: generate_request_ops(txn.success),
36
+ failure: generate_request_ops(txn.failure),
37
+ )
38
+ resp = @stub.txn(request, metadata: @metadata, deadline: deadline(timeout))
39
+ strip_prefix(@namespace, resp)
40
+ end
41
+
42
+ private
43
+
44
+ def deadline(timeout)
45
+ from_relative_time(timeout || @timeout)
46
+ end
47
+
48
+ def generate_request_ops(requests)
49
+ requests.map do |request|
50
+ if request.is_a?(Etcdserverpb::RangeRequest)
51
+ Etcdserverpb::RequestOp.new(request_range: request)
52
+ elsif request.is_a?(Etcdserverpb::PutRequest)
53
+ Etcdserverpb::RequestOp.new(request_put: request)
54
+ elsif request.is_a?(Etcdserverpb::DeleteRangeRequest)
55
+ Etcdserverpb::RequestOp.new(request_delete_range: request)
56
+ else
57
+ raise "Invalid command. Not sure how you got here!"
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,32 @@
1
+ class Etcdv3::Namespace
2
+ class Lock
3
+ include GRPC::Core::TimeConsts
4
+ include Etcdv3::Namespace::Utilities
5
+
6
+ def initialize(hostname, credentials, timeout, namespace, metadata = {})
7
+ @stub = V3lockpb::Lock::Stub.new(hostname, credentials)
8
+ @timeout = timeout
9
+ @namespace = namespace
10
+ @metadata = metadata
11
+ end
12
+
13
+ def lock(name, lease_id, timeout: nil)
14
+ name = prepend_prefix(@namespace, name)
15
+ request = V3lockpb::LockRequest.new(name: name, lease: lease_id)
16
+ resp = @stub.lock(request, deadline: deadline(timeout))
17
+ strip_prefix_from_lock(@namespace, resp)
18
+ end
19
+
20
+ def unlock(key, timeout: nil)
21
+ key = prepend_prefix(@namespace, key)
22
+ request = V3lockpb::UnlockRequest.new(key: key)
23
+ @stub.unlock(request, deadline: deadline(timeout))
24
+ end
25
+
26
+ private
27
+
28
+ def deadline(timeout)
29
+ from_relative_time(timeout || @timeout)
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,44 @@
1
+ class Etcdv3::Namespace
2
+ module Utilities
3
+
4
+ def prepend_prefix(prefix, key)
5
+ key = key.dup if key.frozen?
6
+ key.prepend(prefix)
7
+ end
8
+
9
+ def strip_prefix(prefix, resp)
10
+ [:kvs, :prev_kvs].each do |field|
11
+ if resp.respond_to?(field)
12
+ resp.send(field).each do |kv|
13
+ kv.key = delete_prefix(prefix, kv.key)
14
+ end
15
+ end
16
+ end
17
+ resp
18
+ end
19
+
20
+ def strip_prefix_from_lock(prefix, resp)
21
+ if resp.key
22
+ resp.key = delete_prefix(prefix, resp.key)
23
+ end
24
+ resp
25
+ end
26
+
27
+ def strip_prefix_from_events(prefix, events)
28
+ events.each do |event|
29
+ if event.kv
30
+ event.kv.key = delete_prefix(prefix, event.kv.key)
31
+ end
32
+ if event.prev_kv
33
+ event.prev_kv.key = delete_prefix(prefix, event.prev_kv.key)
34
+ end
35
+ event
36
+ end
37
+ end
38
+
39
+ def delete_prefix(prefix, str)
40
+ str.sub(/\A#{prefix}/, '')
41
+ end
42
+
43
+ end
44
+ end
@@ -0,0 +1,38 @@
1
+ class Etcdv3::Namespace
2
+ class Watch
3
+ include GRPC::Core::TimeConsts
4
+ include Etcdv3::Namespace::Utilities
5
+
6
+ def initialize(hostname, credentials, timeout, namespace, metadata = {})
7
+ @stub = Etcdserverpb::Watch::Stub.new(hostname, credentials)
8
+ @timeout = timeout
9
+ @namespace = namespace
10
+ @metadata = metadata
11
+ end
12
+
13
+ def watch(key, range_end, start_revision, block, timeout: nil)
14
+ key = prepend_prefix(@namespace, key)
15
+ range_end = prepend_prefix(@namespace, range_end) if range_end
16
+ create_req = Etcdserverpb::WatchCreateRequest.new(key: key)
17
+ create_req.range_end = range_end if range_end
18
+ create_req.start_revision = start_revision if start_revision
19
+ watch_req = Etcdserverpb::WatchRequest.new(create_request: create_req)
20
+ events = nil
21
+ @stub.watch([watch_req], metadata: @metadata, deadline: deadline(timeout)).each do |resp|
22
+ next if resp.events.empty?
23
+ if block
24
+ block.call(strip_prefix_from_events(@namespace, resp.events))
25
+ else
26
+ events = strip_prefix_from_events(@namespace, resp.events)
27
+ break
28
+ end
29
+ end
30
+ events
31
+ end
32
+
33
+ def deadline(timeout)
34
+ from_relative_time(timeout || @timeout)
35
+ end
36
+
37
+ end
38
+ end
@@ -1,3 +1,3 @@
1
1
  class Etcdv3
2
- VERSION = '0.10.2'.freeze
2
+ VERSION = '0.11.3'.freeze
3
3
  end
data/lib/etcdv3.rb CHANGED
@@ -7,6 +7,14 @@ require 'etcdv3/auth'
7
7
  require 'etcdv3/kv/requests'
8
8
  require 'etcdv3/kv/transaction'
9
9
  require 'etcdv3/kv'
10
+
11
+ require 'etcdv3/namespace/utilities'
12
+ require 'etcdv3/namespace/kv/requests'
13
+ require 'etcdv3/namespace/kv/transaction'
14
+ require 'etcdv3/namespace/lock'
15
+ require 'etcdv3/namespace/kv'
16
+ require 'etcdv3/namespace/watch'
17
+
10
18
  require 'etcdv3/maintenance'
11
19
  require 'etcdv3/lease'
12
20
  require 'etcdv3/watch'
@@ -24,9 +32,11 @@ class Etcdv3
24
32
  def initialize(**options)
25
33
  @options = options
26
34
  @timeout = options[:command_timeout] || DEFAULT_TIMEOUT
35
+ @namespace = options[:namespace]
27
36
  @conn = ConnectionWrapper.new(
28
37
  @timeout,
29
38
  *sanitized_endpoints,
39
+ @namespace,
30
40
  @options.fetch(:allow_reconnect, true),
31
41
  )
32
42
  warn "WARNING: `url` is deprecated. Please use `endpoints` instead." if @options.key?(:url)
@@ -3,7 +3,7 @@ require 'spec_helper'
3
3
  describe Etcdv3::Connection do
4
4
 
5
5
  describe '#initialize - without metadata' do
6
- subject { Etcdv3::Connection.new('http://localhost:2379', 10) }
6
+ subject { Etcdv3::Connection.new('http://localhost:2379', 10, nil) }
7
7
 
8
8
  it { is_expected.to have_attributes(endpoint: URI('http://localhost:2379')) }
9
9
  it { is_expected.to have_attributes(credentials: :this_channel_is_insecure) }
@@ -22,7 +22,7 @@ describe Etcdv3::Connection do
22
22
  end
23
23
 
24
24
  describe '#initialize - with metadata' do
25
- subject { Etcdv3::Connection.new('http://localhost:2379', 10, token: 'token123') }
25
+ subject { Etcdv3::Connection.new('http://localhost:2379', 10, nil, token: 'token123') }
26
26
 
27
27
  [:kv, :maintenance, :lease, :watch, :auth].each do |handler|
28
28
  let(:handler_stub) { subject.handlers[handler].instance_variable_get(:@stub) }
@@ -37,7 +37,7 @@ describe Etcdv3::Connection do
37
37
  end
38
38
 
39
39
  describe '#refresh_metadata' do
40
- subject { Etcdv3::Connection.new('http://localhost:2379', token: 'token123') }
40
+ subject { Etcdv3::Connection.new('http://localhost:2379', nil, token: 'token123') }
41
41
  before { subject.refresh_metadata(token: 'newtoken') }
42
42
  [:kv, :maintenance, :lease, :watch, :auth].each do |handler|
43
43
  let(:handler_metadata) { subject.handlers[handler].instance_variable_get(:@metadata) }
@@ -3,7 +3,7 @@ require 'spec_helper'
3
3
  describe Etcdv3::ConnectionWrapper do
4
4
  let(:conn) { local_connection }
5
5
  let(:endpoints) { ['http://localhost:2379', 'http://localhost:2389'] }
6
- subject { Etcdv3::ConnectionWrapper.new(10, *endpoints, true) }
6
+ subject { Etcdv3::ConnectionWrapper.new(10, *endpoints, nil, true) }
7
7
 
8
8
  describe '#initialize' do
9
9
  it { is_expected.to have_attributes(user: nil, password: nil, token: nil) }
@@ -0,0 +1,82 @@
1
+ require 'spec_helper'
2
+
3
+ describe Etcdv3::KV do
4
+ let(:stub) { local_namespace_stub(Etcdv3::Namespace::KV, 1, '/namespace/') }
5
+ let(:lease_stub) { local_stub(Etcdv3::Lease, 1) }
6
+
7
+ it_should_behave_like "a method with a GRPC timeout", described_class, :get, :range, "key"
8
+ it_should_behave_like "a method with a GRPC timeout", described_class, :del, :delete_range, "key"
9
+ it_should_behave_like "a method with a GRPC timeout", described_class, :put, :put, "key", "val"
10
+
11
+ it "should timeout transactions" do
12
+ stub = local_namespace_stub(Etcdv3::Namespace::KV, 0, '/namespace/')
13
+ expect { stub.transaction(Proc.new { nil }) }.to raise_error(GRPC::DeadlineExceeded)
14
+ end
15
+
16
+ describe '#put' do
17
+ context 'without lease' do
18
+ subject { stub.put('test', 'test') }
19
+ it { is_expected.to be_an_instance_of(Etcdserverpb::PutResponse) }
20
+ end
21
+
22
+ context 'with lease' do
23
+ let(:lease_id) { lease_stub.lease_grant(1)['ID'] }
24
+ subject { stub.put('lease', 'test', lease: lease_id) }
25
+ it { is_expected.to be_an_instance_of(Etcdserverpb::PutResponse) }
26
+ end
27
+ end
28
+
29
+ describe '#get' do
30
+ subject { stub.get('test') }
31
+ it { is_expected.to be_an_instance_of(Etcdserverpb::RangeResponse) }
32
+ end
33
+
34
+ describe '#del' do
35
+ context 'del without range' do
36
+ subject { stub.del('test') }
37
+ it { is_expected.to be_an_instance_of(Etcdserverpb::DeleteRangeResponse) }
38
+ end
39
+ context 'del with range' do
40
+ subject { stub.del('test', range_end: 'testtt') }
41
+ it { is_expected.to be_an_instance_of(Etcdserverpb::DeleteRangeResponse) }
42
+ end
43
+ end
44
+
45
+ describe '#transaction' do
46
+ context 'put' do
47
+ let!(:block) do
48
+ Proc.new do |txn|
49
+ txn.compare = [ txn.value('txn', :equal, 'value') ]
50
+ txn.success = [ txn.put('txn-test', 'success') ]
51
+ txn.failure = [ txn.put('txn-test', 'failed') ]
52
+ end
53
+ end
54
+ subject { stub.transaction(block) }
55
+ it { is_expected.to be_an_instance_of(Etcdserverpb::TxnResponse) }
56
+ end
57
+
58
+ context 'del' do
59
+ let!(:block) do
60
+ Proc.new do |txn|
61
+ txn.compare = [ txn.value('txn', :equal, 'value') ]
62
+ txn.success = [ txn.del('txn-one') ]
63
+ txn.failure = [ txn.del('txn-two') ]
64
+ end
65
+ end
66
+ subject { stub.transaction(block) }
67
+ it { is_expected.to be_an_instance_of(Etcdserverpb::TxnResponse) }
68
+ end
69
+ end
70
+
71
+ context 'get' do
72
+ let!(:block) do
73
+ Proc.new do |txn|
74
+ txn.compare = [ txn.value('txn', :equal, 'value') ]
75
+ txn.success = [ txn.get('txn-success') ]
76
+ txn.failure = [ txn.get('txn-failure') ]
77
+ end
78
+ end
79
+ subject { stub.transaction(block) }
80
+ it { is_expected.to be_an_instance_of(Etcdserverpb::TxnResponse) }
81
+ end
82
+ end
@@ -0,0 +1,23 @@
1
+ require 'spec_helper'
2
+
3
+ # Locking is not implemented in etcd v3.1.X
4
+ unless $instance.version < Gem::Version.new("3.2.0")
5
+ describe Etcdv3::Lock do
6
+ let(:stub) { local_namespace_stub(Etcdv3::Namespace::Lock, 1, '/namespace/') }
7
+ let(:lease_stub) { local_stub(Etcdv3::Lease, 1) }
8
+
9
+ it_should_behave_like "a method with a GRPC timeout", described_class, :unlock, :unlock, 'foo'
10
+ # it_should_behave_like "a method with a GRPC timeout", described_class, :lock, :lock, 'foo'
11
+
12
+ describe '#lock' do
13
+ let(:lease_id) { lease_stub.lease_grant(10)['ID'] }
14
+ subject { stub.lock('foo', lease_id) }
15
+ it { is_expected.to be_an_instance_of(V3lockpb::LockResponse) }
16
+ end
17
+
18
+ describe '#unlock' do
19
+ subject { stub.unlock('foo') }
20
+ it { is_expected.to be_an_instance_of(V3lockpb::UnlockResponse) }
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,36 @@
1
+ require 'spec_helper'
2
+ require 'securerandom'
3
+
4
+
5
+ describe Etcdv3::Namespace::Watch do
6
+ let(:stub) { local_namespace_stub(Etcdv3::Namespace::Watch, 5, '/namespace/') }
7
+ let(:kv_stub_no_ns) { local_stub(Etcdv3::KV, 1) }
8
+ let(:kv_stub) { local_namespace_stub(Etcdv3::Namespace::KV, 1, '/namespace/') }
9
+
10
+ context 'watch' do
11
+ it 'should return an event' do
12
+ resp = nil
13
+ thr = Thread.new do |thr|
14
+ resp = stub.watch("foo", nil, 1, nil)
15
+ end
16
+ sleep 2
17
+ kv_stub.put("foo", "works")
18
+ thr.join
19
+ expect(resp).to be_an_instance_of(Google::Protobuf::RepeatedField)
20
+ expect(resp.last.kv.key).to eq('foo')
21
+ end
22
+
23
+ it 'should return event when non-namespace client writes to key' do
24
+ resp = nil
25
+ thr = Thread.new do |thr|
26
+ resp = stub.watch("foobar", nil, 1, nil)
27
+ end
28
+ sleep 2
29
+ kv_stub_no_ns.put("/namespace/foobar", "works")
30
+ thr.join
31
+ expect(resp).to be_an_instance_of(Google::Protobuf::RepeatedField)
32
+ expect(resp.last.kv.key).to eq('foobar')
33
+ end
34
+ end
35
+ end
36
+
@@ -4,14 +4,21 @@ require 'securerandom'
4
4
  # Locking is not implemented in etcd v3.1.X
5
5
  unless $instance.version < Gem::Version.new("3.2.0")
6
6
  describe Etcdv3::Watch do
7
- let(:stub) { local_stub(Etcdv3::Watch, 1) }
7
+ let(:stub) { local_stub(Etcdv3::Watch, 5) }
8
8
  let(:kv_stub) { local_stub(Etcdv3::KV, 1) }
9
9
 
10
- context 'xxx' do
11
- before(:each) do
12
- kv_stub.put 'foo', 'bar'
10
+ context 'watch' do
11
+ it 'should return an event' do
12
+ resp = nil
13
+ thr = Thread.new do |thr|
14
+ resp = stub.watch("foo", nil, 1, nil)
15
+ end
16
+ sleep 2
17
+ kv_stub.put("foo", "works")
18
+ thr.join
19
+ expect(resp).to be_an_instance_of(Google::Protobuf::RepeatedField)
20
+ expect(resp.last.kv.key).to eq('foo')
13
21
  end
14
- it_should_behave_like "a method with a GRPC timeout", described_class, :watch, :watch, 'foo', "\0", 1, nil
15
22
  end
16
23
  end
17
24
  end
data/spec/etcdv3_spec.rb CHANGED
@@ -521,5 +521,261 @@ describe Etcdv3 do
521
521
  end
522
522
  end
523
523
  end
524
+
525
+ describe "namespace" do
526
+
527
+ describe '#get' do
528
+ let(:get_conn) { local_connection_with_namespace("/namespace-get/") }
529
+
530
+ before do
531
+ conn.put('/apples/', 'app')
532
+ conn.put('/namespace-get/apple', 'apple')
533
+ conn.put('/namespace-get/apples', 'apples')
534
+ conn.put('/namespace-get/appless', 'appless')
535
+ end
536
+
537
+ it 'returns key w/o namespace' do
538
+ expect(get_conn.get("apple").kvs.last.value).to eq('apple')
539
+ end
540
+
541
+ it 'returns keys w/o namespace' do
542
+ expect(get_conn.get("apple", range_end: 'applf').kvs.size).to eq(3)
543
+ end
544
+
545
+ it 'returns all keys under namespace' do
546
+ expect(get_conn.get("", range_end: "\0").kvs.size).to eq(3)
547
+ end
548
+ end
549
+
550
+ describe '#put' do
551
+ let(:put_conn) { local_connection_with_namespace("/namespace-put/") }
552
+
553
+ before do
554
+ put_conn.put('apple_put', 'test')
555
+ end
556
+ it 'returns key with namespace' do
557
+ expect(conn.get("/namespace-put/apple_put").kvs.last.value).to eq('test')
558
+ end
559
+ end
560
+
561
+ describe '#del' do
562
+ let(:del_conn) { local_connection_with_namespace("/del-test/") }
563
+
564
+ context 'zero-byte' do
565
+ before do
566
+ del_conn.put('test', "key")
567
+ del_conn.put('test2', "key2")
568
+ conn.put('wall', 'zzzz')
569
+ conn.put('walzz', 'adsfas')
570
+ end
571
+
572
+ it 'deleting all keys should be scoped to namespace' do
573
+ resp = del_conn.del("", range_end: "\0")
574
+ expect(resp.deleted).to eq(2)
575
+ expect(conn.get("wall").kvs.last.value).to eq('zzzz')
576
+ end
577
+ end
578
+
579
+ context 'no range' do
580
+ before { del_conn.put('test', 'value') }
581
+ subject { del_conn.del('test') }
582
+ it { is_expected.to_not be_nil }
583
+ end
584
+
585
+ context 'ranged del' do
586
+ before do
587
+ del_conn.put('test', 'value')
588
+ del_conn.put('testt', 'value')
589
+ end
590
+ subject { del_conn.del('test', range_end: 'testtt') }
591
+ it { is_expected.to_not be_nil }
592
+ end
593
+ end
594
+
595
+ describe '#transaction' do
596
+ let(:trans_conn) { local_connection_with_namespace("/namespace/") }
597
+
598
+ describe 'txn.value' do
599
+ before { trans_conn.put('txn', 'value') }
600
+ after { trans_conn.del('txn') }
601
+ context 'success' do
602
+ subject! do
603
+ trans_conn.transaction do |txn|
604
+ txn.compare = [ txn.value('txn', :equal, 'value') ]
605
+ txn.success = [ txn.put('txn-test', 'success') ]
606
+ txn.failure = [ txn.put('txn-test', 'failed') ]
607
+ end
608
+ end
609
+ it 'sets correct key' do
610
+ expect(trans_conn.get('txn-test').kvs.first.value).to eq('success')
611
+ expect(conn.get("/namespace/txn-test").kvs.first.value).to eq('success')
612
+ end
613
+ it "raises a GRPC::DeadlineExceeded exception when it takes too long" do
614
+ expect do
615
+ trans_conn.transaction(timeout: 0) do |txn|
616
+ txn.compare = [ txn.value('txn', :equal, 'value') ]
617
+ txn.success = [ txn.put('txn-test', 'success') ]
618
+ txn.failure = [ txn.put('txn-test', 'failed') ]
619
+ end
620
+ end.to raise_exception(GRPC::DeadlineExceeded)
621
+ end
622
+ it "accepts a timeout" do
623
+ expect do
624
+ trans_conn.transaction(timeout: 1) do |txn|
625
+ txn.compare = [ txn.value('txn', :equal, 'value') ]
626
+ txn.success = [ txn.put('txn-test', 'success') ]
627
+ txn.failure = [ txn.put('txn-test', 'failed') ]
628
+ end
629
+ end.to_not raise_exception
630
+ end
631
+ end
632
+ context "success, value with lease" do
633
+ let!(:lease_id) { trans_conn.lease_grant(2)['ID'] }
634
+ subject! do
635
+ trans_conn.transaction do |txn|
636
+ txn.compare = [ txn.value('txn', :equal, 'value') ]
637
+ txn.success = [ txn.put('txn-test', 'success', lease_id) ]
638
+ txn.failure = [ txn.put('txn-test', 'failed', lease_id) ]
639
+ end
640
+ end
641
+ it 'sets correct key, with a lease' do
642
+ expect(trans_conn.get('txn-test').kvs.first.value).to eq('success')
643
+ expect(trans_conn.get('txn-test').kvs.first.lease).to eq(lease_id)
644
+ end
645
+ end
646
+ context 'failure' do
647
+ subject! do
648
+ trans_conn.transaction do |txn|
649
+ txn.compare = [ txn.value('txn', :equal, 'notright') ]
650
+ txn.success = [ txn.put('txn-test', 'success') ]
651
+ txn.failure = [ txn.put('txn-test', 'failed') ]
652
+ end
653
+ end
654
+ it 'sets correct key' do
655
+ expect(trans_conn.get('txn-test').kvs.first.value).to eq('failed')
656
+ end
657
+ end
658
+ end
659
+
660
+ describe 'txn.create_revision' do
661
+ before { trans_conn.put('txn', 'value') }
662
+ after { trans_conn.del('txn') }
663
+ context 'success' do
664
+ subject! do
665
+ trans_conn.transaction do |txn|
666
+ txn.compare = [ txn.create_revision('txn', :greater, 1) ]
667
+ txn.success = [ txn.put('txn-test', 'success') ]
668
+ txn.failure = [ txn.put('txn-test', 'failed') ]
669
+ end
670
+ end
671
+ it 'sets correct key' do
672
+ expect(trans_conn.get('txn-test').kvs.first.value).to eq('success')
673
+ end
674
+ end
675
+ context 'failure' do
676
+ subject! do
677
+ trans_conn.transaction do |txn|
678
+ txn.compare = [ txn.create_revision('txn', :equal, 1) ]
679
+ txn.success = [ txn.put('txn-test', 'success') ]
680
+ txn.failure = [ txn.put('txn-test', 'failed') ]
681
+ end
682
+ end
683
+ it 'sets correct key' do
684
+ expect(trans_conn.get('txn-test').kvs.first.value).to eq('failed')
685
+ expect(conn.get('/namespace/txn-test').kvs.first.value).to eq('failed')
686
+ end
687
+ end
688
+ end
689
+
690
+ describe 'txn.mod_revision' do
691
+ before { trans_conn.put('txn', 'value') }
692
+ after { trans_conn.del('txn') }
693
+ context 'success' do
694
+ subject! do
695
+ trans_conn.transaction do |txn|
696
+ txn.compare = [ txn.mod_revision('txn', :less, 1000) ]
697
+ txn.success = [ txn.put('txn-test', 'success') ]
698
+ txn.failure = [ txn.put('txn-test', 'failed') ]
699
+ end
700
+ end
701
+ it 'sets correct key' do
702
+ expect(trans_conn.get('txn-test').kvs.first.value).to eq('success')
703
+ expect(conn.get('/namespace/txn-test').kvs.first.value).to eq('success')
704
+ end
705
+ end
706
+ context 'failure' do
707
+ subject! do
708
+ trans_conn.transaction do |txn|
709
+ txn.compare = [ txn.mod_revision('txn', :greater, 1000) ]
710
+ txn.success = [ txn.put('txn-test', 'success') ]
711
+ txn.failure = [ txn.put('txn-test', 'failed') ]
712
+ end
713
+ end
714
+ it 'sets correct key' do
715
+ expect(trans_conn.get('txn-test').kvs.first.value).to eq('failed')
716
+ expect(conn.get('/namespace/txn-test').kvs.first.value).to eq('failed')
717
+
718
+ end
719
+ end
720
+ end
721
+
722
+ describe 'txn.version' do
723
+ before { trans_conn.put('txn-version', 'value') }
724
+ after { trans_conn.del('txn-version') }
725
+ context 'success' do
726
+ subject! do
727
+ trans_conn.transaction do |txn|
728
+ txn.compare = [ txn.version('txn-version', :equal, 1) ]
729
+ txn.success = [ txn.put('txn-test', 'success') ]
730
+ txn.failure = [ txn.put('txn-test', 'failed') ]
731
+ end
732
+ end
733
+ it 'sets correct key' do
734
+ expect(trans_conn.get('txn-test').kvs.first.value).to eq('success')
735
+ expect(conn.get('/namespace/txn-test').kvs.first.value).to eq('success')
736
+ end
737
+ end
738
+ context 'failure' do
739
+ subject! do
740
+ trans_conn.transaction do |txn|
741
+ txn.compare = [ txn.version('txn', :equal, 100)]
742
+ txn.success = [ txn.put('txn-test', 'success') ]
743
+ txn.failure = [ txn.put('txn-test', 'failed') ]
744
+ end
745
+ end
746
+ it 'sets correct key' do
747
+ expect(trans_conn.get('txn-test').kvs.first.value).to eq('failed')
748
+ expect(conn.get('/namespace/txn-test').kvs.first.value).to eq('failed')
749
+ end
750
+ end
751
+ end
752
+ end
753
+
754
+ # Locking is not implemented in etcd v3.1.X
755
+ unless $instance.version < Gem::Version.new("3.2.0")
756
+ describe "locking" do
757
+ let(:ns_conn) { local_connection_with_namespace("/namespace/") }
758
+
759
+ describe '#lock' do
760
+ let(:lease_id) { lease_stub.lease_grant(10)['ID'] }
761
+ subject { ns_conn.lock('mylocklock', lease_id) }
762
+ it 'should lock key under specified namespace' do
763
+ expect(conn.get("/namespace/#{subject.key}").kvs).to_not be_empty
764
+ end
765
+ end
766
+
767
+ describe '#with_lock' do
768
+ let(:lease_id) { lease_stub.lease_grant(10)['ID'] }
769
+ let(:lease_id_2) { lease_stub.lease_grant(15)['ID'] }
770
+ it 'enforces lock' do
771
+ ns_conn.with_lock('mylock', lease_id) do
772
+ expect { ns_conn.lock('mylock', lease_id_2, timeout: 0.1) }
773
+ .to raise_error(GRPC::DeadlineExceeded)
774
+ end
775
+ end
776
+ end
777
+ end
778
+ end
779
+ end
524
780
  end
525
781
  end
@@ -13,10 +13,18 @@ module Helpers
13
13
  Etcdv3.new(endpoints: "http://#{local_url}", command_timeout: timeout)
14
14
  end
15
15
 
16
+ def local_connection_with_namespace(namespace)
17
+ Etcdv3.new(endpoints: "http://#{local_url}", namespace: namespace)
18
+ end
19
+
16
20
  def local_stub(interface, timeout=nil)
17
21
  interface.new(local_url, :this_channel_is_insecure, timeout, {})
18
22
  end
19
23
 
24
+ def local_namespace_stub(interface, timeout=nil, namespace)
25
+ interface.new(local_url, :this_channel_is_insecure, timeout, namespace, {})
26
+ end
27
+
20
28
  def local_url
21
29
  "127.0.0.1:#{port}"
22
30
  end
data/spec/spec_helper.rb CHANGED
@@ -1,10 +1,12 @@
1
1
  $LOAD_PATH.unshift File.expand_path('../lib', __FILE__)
2
2
  $LOAD_PATH.unshift File.expand_path('./helpers', __FILE__)
3
+ $LOAD_PATH.unshift File.expand_path('./namespace', __FILE__)
3
4
 
4
- require 'simplecov'
5
- require 'codecov'
6
- SimpleCov.start
7
- SimpleCov.formatter = SimpleCov::Formatter::Codecov
5
+
6
+ # require 'simplecov'
7
+ # require 'codecov'
8
+ # SimpleCov.start
9
+ # SimpleCov.formatter = SimpleCov::Formatter::Codecov
8
10
 
9
11
  require 'etcdv3'
10
12
  require 'helpers/test_instance'
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.10.2
4
+ version: 0.11.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Shaun Davis
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2019-03-15 00:00:00.000000000 Z
11
+ date: 2021-08-02 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: grpc
@@ -97,6 +97,12 @@ files:
97
97
  - lib/etcdv3/lease.rb
98
98
  - lib/etcdv3/lock.rb
99
99
  - lib/etcdv3/maintenance.rb
100
+ - lib/etcdv3/namespace/kv.rb
101
+ - lib/etcdv3/namespace/kv/requests.rb
102
+ - lib/etcdv3/namespace/kv/transaction.rb
103
+ - lib/etcdv3/namespace/lock.rb
104
+ - lib/etcdv3/namespace/utilities.rb
105
+ - lib/etcdv3/namespace/watch.rb
100
106
  - lib/etcdv3/protos/annotations.proto
101
107
  - lib/etcdv3/protos/auth.proto
102
108
  - lib/etcdv3/protos/descriptor.proto
@@ -114,6 +120,9 @@ files:
114
120
  - spec/etcdv3/lease_spec.rb
115
121
  - spec/etcdv3/lock_spec.rb
116
122
  - spec/etcdv3/maintenance_spec.rb
123
+ - spec/etcdv3/namespace/kv_spec.rb
124
+ - spec/etcdv3/namespace/lock_spec.rb
125
+ - spec/etcdv3/namespace/watch_spec.rb
117
126
  - spec/etcdv3/watch_spec.rb
118
127
  - spec/etcdv3_spec.rb
119
128
  - spec/helpers/connections.rb
@@ -139,8 +148,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
139
148
  - !ruby/object:Gem::Version
140
149
  version: '0'
141
150
  requirements: []
142
- rubyforge_project:
143
- rubygems_version: 2.4.5.1
151
+ rubygems_version: 3.2.20
144
152
  signing_key:
145
153
  specification_version: 4
146
154
  summary: A Etcd client library for Version 3
@@ -152,6 +160,9 @@ test_files:
152
160
  - spec/etcdv3/lease_spec.rb
153
161
  - spec/etcdv3/lock_spec.rb
154
162
  - spec/etcdv3/maintenance_spec.rb
163
+ - spec/etcdv3/namespace/kv_spec.rb
164
+ - spec/etcdv3/namespace/lock_spec.rb
165
+ - spec/etcdv3/namespace/watch_spec.rb
155
166
  - spec/etcdv3/watch_spec.rb
156
167
  - spec/etcdv3_spec.rb
157
168
  - spec/helpers/connections.rb