etcdv3 0.10.1 → 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: eea828b2c14cfeab2c0f9be8a84ea937131a5dc2
4
- data.tar.gz: a2e02f9d0155f35185994c07cd7abc5a6d23d5df
2
+ SHA256:
3
+ metadata.gz: 59585a18eada27829edbf162733146e90e4c336bcbbefa761b3063e3f9400139
4
+ data.tar.gz: edc026da35b1396e664a431a79eb2012238ea4bd49c8ef43e1f70f93366935fe
5
5
  SHA512:
6
- metadata.gz: bff96b426657924163a26df9ca1322b5600b33607453d4648affffe2e66bf7c7ef20e5fe2d0443435de71b74d9ceec637f968196315837426294d26dd1b84e78
7
- data.tar.gz: 1a10f332e633b1010aba3a355b05ef82e2546e37413222de7553bcdb2461d0d2b368573af75a33e927349c4f461c81564d80adbe149ea7f6e5ac8b478158cc1e
6
+ metadata.gz: d5afcdcad7f96e422bebcd6d4d4db148bbe48c8600be017650ae229d988cd2e5c6d3547f2026eb1db18a10c91643ba3e84bb0a2555b2f8add7f687dd9bfc014a
7
+ data.tar.gz: 310680f7f8f11b65dfb9ea1a116536394678faf05f29087a77b87b1665138f85c4bc39614299dff22a528a63f0a0771dbb3841e9b60a610611dacff33048fbf1
data/.gitignore CHANGED
@@ -1,9 +1,10 @@
1
- Gemfile.lock
2
- Guardfile
3
- *.sh
4
1
  *.gem
5
- /tmp/
2
+ *.sh
3
+ *.swp
4
+ /.bundle
6
5
  /.idea
7
6
  /coverage/*
8
7
  /doc/
9
- *.swp
8
+ /tmp/
9
+ Gemfile.lock
10
+ Guardfile
data/README.md CHANGED
@@ -26,6 +26,9 @@ 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
 
@@ -34,6 +37,37 @@ conn = Etcdv3.new(endpoints: 'https://hostname:port', user: 'root', password: 'm
34
37
 
35
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.
36
39
 
40
+ However, sometimes this is not what you want. If you need more control over
41
+ failures, you can suppress this mechanism by using
42
+
43
+ ```ruby
44
+ conn = Etcdv3.new(endpoints: 'https://hostname:port', allow_reconnect: false)
45
+ ```
46
+
47
+ This will still rotate the endpoints, but it will raise an exception so you can
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
+
37
71
 
38
72
  ## Adding, Fetching and Deleting Keys
39
73
  ```ruby
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'
@@ -21,10 +29,16 @@ class Etcdv3
21
29
  attr_reader :conn, :credentials, :options
22
30
  DEFAULT_TIMEOUT = 120
23
31
 
24
- def initialize(options = {})
32
+ def initialize(**options)
25
33
  @options = options
26
34
  @timeout = options[:command_timeout] || DEFAULT_TIMEOUT
27
- @conn = ConnectionWrapper.new(@timeout, *sanitized_endpoints)
35
+ @namespace = options[:namespace]
36
+ @conn = ConnectionWrapper.new(
37
+ @timeout,
38
+ *sanitized_endpoints,
39
+ @namespace,
40
+ @options.fetch(:allow_reconnect, true),
41
+ )
28
42
  warn "WARNING: `url` is deprecated. Please use `endpoints` instead." if @options.key?(:url)
29
43
  authenticate(@options[:user], @options[:password]) if @options.key?(:user)
30
44
  end
@@ -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,31 +3,40 @@ class Etcdv3
3
3
 
4
4
  attr_accessor :connection, :endpoints, :user, :password, :token, :timeout
5
5
 
6
- def initialize(timeout, *endpoints)
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) }
9
+ @namespace = namespace
10
+ @endpoints = endpoints.map{|endpoint| Etcdv3::Connection.new(endpoint, @timeout, @namespace) }
11
+ @allow_reconnect = allow_reconnect
10
12
  @connection = @endpoints.first
11
13
  end
12
14
 
15
+ private def retry_or_raise(*args)
16
+ if @allow_reconnect
17
+ handle(*args)
18
+ else
19
+ raise
20
+ end
21
+ end
22
+
13
23
  def handle(stub, method, method_args=[], retries: 1)
14
24
  @connection.call(stub, method, method_args)
15
25
 
16
- rescue GRPC::Unavailable, GRPC::Core::CallError => exception
26
+ rescue GRPC::Unavailable, GRPC::Core::CallError
17
27
  $stderr.puts("Failed to connect to endpoint '#{@connection.hostname}'")
18
28
  if @endpoints.size > 1
19
29
  rotate_connection_endpoint
20
- $stderr.puts("Failover event triggered. Failing over to '#{@connection.hostname}'")
21
- return handle(stub, method, method_args)
30
+ return retry_or_raise(stub, method, method_args)
22
31
  else
23
- return handle(stub, method, method_args)
32
+ return retry_or_raise(stub, method, method_args)
24
33
  end
25
34
  rescue GRPC::Unauthenticated => exception
26
35
  # Regenerate token in the event it expires.
27
36
  if exception.details == 'etcdserver: invalid auth token'
28
37
  if retries > 0
29
38
  authenticate(@user, @password)
30
- return handle(stub, method, method_args, retries: retries - 1)
39
+ return retry_or_raise(stub, method, method_args, retries: retries - 1)
31
40
  end
32
41
  end
33
42
  raise exception
@@ -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,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,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.1'.freeze
2
+ VERSION = '0.11.3'.freeze
3
3
  end
@@ -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,9 +3,9 @@ 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, nil, true) }
6
7
 
7
8
  describe '#initialize' do
8
- subject { Etcdv3::ConnectionWrapper.new(10, *endpoints) }
9
9
  it { is_expected.to have_attributes(user: nil, password: nil, token: nil) }
10
10
  it 'sets hostnames in correct order' do
11
11
  expect(subject.endpoints.map(&:hostname)).to eq(['localhost:2379', 'localhost:2389'])
@@ -16,7 +16,6 @@ describe Etcdv3::ConnectionWrapper do
16
16
  end
17
17
 
18
18
  describe "#rotate_connection_endpoint" do
19
- subject { Etcdv3::ConnectionWrapper.new(10, *endpoints) }
20
19
  before do
21
20
  subject.rotate_connection_endpoint
22
21
  end
@@ -29,11 +28,30 @@ describe Etcdv3::ConnectionWrapper do
29
28
  end
30
29
 
31
30
  describe "Failover Simulation" do
32
- let(:modified_conn) { local_connection("http://localhost:2369, http://localhost:2379") }
31
+ let(:allow_reconnect) { true }
32
+ let(:modified_conn) {
33
+ local_connection(
34
+ "http://localhost:2369, http://localhost:2379",
35
+ allow_reconnect: allow_reconnect
36
+ )
37
+ }
38
+ subject { modified_conn.get('boom') }
39
+
33
40
  context 'without auth' do
34
41
  # 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) }
42
+ context 'with reconnect' do
43
+ it { is_expected.to be_an_instance_of(Etcdserverpb::RangeResponse) }
44
+ end
45
+ context 'without reconnect' do
46
+ let(:allow_reconnect) { false }
47
+ it { expect { subject }.to raise_error(GRPC::Unavailable) }
48
+ end
49
+ context 'without reconnect with single endpoint' do
50
+ let(:modified_conn) {
51
+ local_connection("http://localhost:2369",allow_reconnect: false)
52
+ }
53
+ it { expect { subject }.to raise_error(GRPC::Unavailable) }
54
+ end
37
55
  end
38
56
  context 'with auth' do
39
57
  before do
@@ -50,12 +68,19 @@ describe Etcdv3::ConnectionWrapper do
50
68
  modified_conn.auth_disable
51
69
  modified_conn.user_delete('root')
52
70
  end
53
- subject { modified_conn.get('boom') }
54
- it { is_expected.to be_an_instance_of(Etcdserverpb::RangeResponse) }
71
+ context 'with reconnect' do
72
+ it { is_expected.to be_an_instance_of(Etcdserverpb::RangeResponse) }
73
+ end
74
+ context 'without reconnect' do
75
+ let(:allow_reconnect) { false }
76
+ it { expect { subject }.to raise_error(GRPC::Unavailable) }
77
+ end
55
78
  end
56
79
  end
57
80
 
58
81
  describe "GRPC::Unauthenticated recovery" do
82
+ let(:allow_reconnect) { true }
83
+ let(:conn) { local_connection(allow_reconnect: allow_reconnect) }
59
84
  let(:wrapper) { conn.send(:conn) }
60
85
  let(:connection) { wrapper.connection }
61
86
  before do
@@ -71,6 +96,12 @@ describe Etcdv3::ConnectionWrapper do
71
96
  conn.user_delete('root')
72
97
  end
73
98
  subject { conn.user_get('root') }
74
- it { is_expected.to be_an_instance_of(Etcdserverpb::AuthUserGetResponse) }
99
+ context 'with reconnect' do
100
+ it { is_expected.to be_an_instance_of(Etcdserverpb::AuthUserGetResponse) }
101
+ end
102
+ context 'without reconnect' do
103
+ let(:allow_reconnect) { false }
104
+ it { expect { subject }.to raise_error(GRPC::Unauthenticated) }
105
+ end
75
106
  end
76
107
  end
@@ -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
@@ -5,18 +5,26 @@ module Helpers
5
5
  Etcdv3.new(endpoints: "http://#{local_url}", user: user, password: password)
6
6
  end
7
7
 
8
- def local_connection(endpoints="http://#{local_url}")
9
- Etcdv3.new(endpoints: endpoints)
8
+ def local_connection(endpoints="http://#{local_url}", allow_reconnect: true)
9
+ Etcdv3.new(endpoints: endpoints, allow_reconnect: allow_reconnect)
10
10
  end
11
11
 
12
12
  def local_connection_with_timeout(timeout)
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.1
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-01-29 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