imperium 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +11 -0
- data/.travis.yml +8 -0
- data/Dockerfile.ci +19 -0
- data/Gemfile +4 -0
- data/README.md +90 -0
- data/Rakefile +6 -0
- data/bin/console +14 -0
- data/bin/rspec +17 -0
- data/bin/setup +8 -0
- data/build.sh +5 -0
- data/docker-compose.override.yml.example +9 -0
- data/docker-compose.yml +20 -0
- data/imperium.gemspec +34 -0
- data/lib/imperium.rb +22 -0
- data/lib/imperium/client.rb +58 -0
- data/lib/imperium/configuration.rb +76 -0
- data/lib/imperium/error.rb +5 -0
- data/lib/imperium/http_client.rb +41 -0
- data/lib/imperium/kv.rb +92 -0
- data/lib/imperium/kv_get_response.rb +102 -0
- data/lib/imperium/kv_pair.rb +66 -0
- data/lib/imperium/response.rb +50 -0
- data/lib/imperium/version.rb +3 -0
- metadata +193 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: '0843f656e4b5c634136bdcddd59a8a81528ca45b'
|
4
|
+
data.tar.gz: 85309c76a17789e23090a36c8d076c119755d1c9
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 1e2acf912515f5ebd12282d4f7f6f13a164b386741a0578c8a2180d21f8828552c1ed6c480396ce057486fa7fb8d56e5315be11fc08b306eeaa0c8317e516c7f
|
7
|
+
data.tar.gz: 2538b80f19905a4b360252681615fce837b4f28e5c4a9e59c3b42914454e2e303756fe67818aaf1882c9dc82d2812b2a3cf0e5d513599a75e113b6b33d80efe2
|
data/.gitignore
ADDED
data/.travis.yml
ADDED
data/Dockerfile.ci
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
FROM instructure/rvm
|
2
|
+
|
3
|
+
WORKDIR /app
|
4
|
+
|
5
|
+
COPY imperium.gemspec Gemfile* /app/
|
6
|
+
COPY lib/imperium/version.rb /app/lib/imperium/version.rb
|
7
|
+
|
8
|
+
USER root
|
9
|
+
RUN chown -R docker:docker /app
|
10
|
+
USER docker
|
11
|
+
|
12
|
+
RUN /bin/bash -l -c "cd /app && bundle install"
|
13
|
+
COPY . /app
|
14
|
+
|
15
|
+
USER root
|
16
|
+
RUN chown -R docker:docker /app
|
17
|
+
USER docker
|
18
|
+
|
19
|
+
CMD /bin/bash -l -c "bundle exec wwtd --parallel"
|
data/Gemfile
ADDED
data/README.md
ADDED
@@ -0,0 +1,90 @@
|
|
1
|
+
# Imperium
|
2
|
+
|
3
|
+
Imperium is a Latin word which roughly translates to 'power to command'. It was
|
4
|
+
often applied to official, and unofficial, positions of power. In this case,
|
5
|
+
specifically the office of Consul.
|
6
|
+
|
7
|
+
Imperium is a Consul client for Ruby applications, it aims to be as ergonomic
|
8
|
+
as possible for users while giving the flexibility required for complex
|
9
|
+
applications. At first only the KV store will be supported but additional
|
10
|
+
functionality is expected to be added as needed (or as pull requests are
|
11
|
+
submitted).
|
12
|
+
|
13
|
+
## Motivation.
|
14
|
+
As Instructure's use of Consul has grown so have our wants and needs in a client
|
15
|
+
library have grown. The goal of this gem is to provide a lightweight, thread
|
16
|
+
safe interface to the full power of Consul's API while not forcing the consumer
|
17
|
+
to use all of it where unnecessary. For now we're focusing on the KV store since
|
18
|
+
most of our use revolves around it.
|
19
|
+
|
20
|
+
## Installation
|
21
|
+
|
22
|
+
Add this line to your application's Gemfile:
|
23
|
+
|
24
|
+
```ruby
|
25
|
+
gem 'imperium'
|
26
|
+
```
|
27
|
+
|
28
|
+
And then execute:
|
29
|
+
|
30
|
+
$ bundle
|
31
|
+
|
32
|
+
Or install it yourself as:
|
33
|
+
|
34
|
+
$ gem install imperium
|
35
|
+
|
36
|
+
## Usage
|
37
|
+
|
38
|
+
Configure:
|
39
|
+
|
40
|
+
```
|
41
|
+
# The following configuration values are used for the default client for each
|
42
|
+
# service. This isn't the only way to get a client set up but will fill the
|
43
|
+
# needs of most applications.
|
44
|
+
Imperium.configure do |config|
|
45
|
+
# Connection values can be specified separately
|
46
|
+
config.host = 'consul.example.com'
|
47
|
+
config.port = 8585
|
48
|
+
config.ssl = false
|
49
|
+
|
50
|
+
# Or, as a url (this is equivilant to the example above).
|
51
|
+
config.url = 'http://consul.example.com:8585'
|
52
|
+
|
53
|
+
confg.token = 'super-sekret-value'
|
54
|
+
end
|
55
|
+
|
56
|
+
# If you want a client that uses some other configuration values without altering
|
57
|
+
# the default ones you can directly instantiate a Configuration object:
|
58
|
+
|
59
|
+
config = Imperium::Configuration.new(url: 'https://other-consul.example.com', token: 'foobar')
|
60
|
+
# This client will contact other-consul.example.com rather than the one configured above.
|
61
|
+
kv_client = Imperium::KV.new(config)
|
62
|
+
```
|
63
|
+
|
64
|
+
GET values from the KV store:
|
65
|
+
```
|
66
|
+
# Get a single value
|
67
|
+
response = Imperium::KV.get('config/single-value', :stale)
|
68
|
+
response.values # => 'qux'
|
69
|
+
|
70
|
+
# Get a set of nested values
|
71
|
+
response = Imperium::KV.get('config/complex-value', :recurse)
|
72
|
+
response.values # => {first: 'value', second: 'value'}
|
73
|
+
```
|
74
|
+
|
75
|
+
## Development
|
76
|
+
|
77
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run
|
78
|
+
`bin/rspec` to run the tests. You can also run `bin/console` for an interactive
|
79
|
+
prompt that will allow you to experiment.
|
80
|
+
|
81
|
+
To install this gem onto your local machine, run `bundle exec rake install`.
|
82
|
+
To release a new version, update the version number in `version.rb`, and then
|
83
|
+
run `bundle exec rake release`, which will create a git tag for the version,
|
84
|
+
push git commits and tags, and push the `.gem` file to
|
85
|
+
[rubygems.org](https://rubygems.org).
|
86
|
+
|
87
|
+
## Contributing
|
88
|
+
|
89
|
+
Bug reports and pull requests are welcome on GitHub at
|
90
|
+
https://github.com/instructure/imperium.
|
data/Rakefile
ADDED
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "imperium"
|
5
|
+
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
8
|
+
|
9
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
+
# require "pry"
|
11
|
+
# Pry.start
|
12
|
+
|
13
|
+
require "irb"
|
14
|
+
IRB.start
|
data/bin/rspec
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
#
|
4
|
+
# This file was generated by Bundler.
|
5
|
+
#
|
6
|
+
# The application 'rspec' is installed as part of a gem, and
|
7
|
+
# this file is here to facilitate running it.
|
8
|
+
#
|
9
|
+
|
10
|
+
require "pathname"
|
11
|
+
ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
|
12
|
+
Pathname.new(__FILE__).realpath)
|
13
|
+
|
14
|
+
require "rubygems"
|
15
|
+
require "bundler/setup"
|
16
|
+
|
17
|
+
load Gem.bin_path("rspec-core", "rspec")
|
data/bin/setup
ADDED
data/build.sh
ADDED
data/docker-compose.yml
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
version: "2"
|
2
|
+
|
3
|
+
services:
|
4
|
+
app:
|
5
|
+
build:
|
6
|
+
context: .
|
7
|
+
dockerfile: Dockerfile.ci
|
8
|
+
links:
|
9
|
+
- consul
|
10
|
+
environment:
|
11
|
+
IMPERIUM_CONSUL_HOST: "consul"
|
12
|
+
IMPERIUM_CONSUL_PORT: 8500
|
13
|
+
IMPERIUM_CONSUL_SSL: "false"
|
14
|
+
|
15
|
+
consul:
|
16
|
+
image: consul:0.7.2
|
17
|
+
command: agent -dev -client 0.0.0.0 -datacenter imperium-dev -node imperium-consul -bootstrap
|
18
|
+
environment:
|
19
|
+
GOMAXPROCS: "2"
|
20
|
+
VIRTUAL_PORT: 8500
|
data/imperium.gemspec
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'imperium/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = 'imperium'
|
8
|
+
spec.version = Imperium::VERSION
|
9
|
+
spec.authors = ['Tyler Pickett']
|
10
|
+
spec.email = ['t.pickett66@gmail.com']
|
11
|
+
|
12
|
+
spec.summary = %q{A powerful, easy to use, Consul client}
|
13
|
+
spec.description = %q{A powerful, easy to use, Consul client}
|
14
|
+
spec.homepage = 'https://github.com/instructure/imperium'
|
15
|
+
spec.license = 'MIT'
|
16
|
+
|
17
|
+
spec.files = `git ls-files -z`.split("\x0").reject do |f|
|
18
|
+
f.match(%r{^(test|spec|features)/})
|
19
|
+
end
|
20
|
+
spec.bindir = 'exe'
|
21
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
22
|
+
spec.require_paths = ['lib']
|
23
|
+
|
24
|
+
spec.add_dependency 'addressable', '~> 2.5.0'
|
25
|
+
spec.add_dependency 'httpclient', '~> 2.8'
|
26
|
+
|
27
|
+
spec.add_development_dependency 'bundler', '~> 1.13'
|
28
|
+
spec.add_development_dependency 'byebug'
|
29
|
+
spec.add_development_dependency 'rake', '~> 10.0'
|
30
|
+
spec.add_development_dependency 'rspec', '~> 3.0'
|
31
|
+
spec.add_development_dependency 'webmock', '~> 2.3.2'
|
32
|
+
spec.add_development_dependency 'wwtd', '~> 1.3'
|
33
|
+
spec.add_development_dependency 'yard'
|
34
|
+
end
|
data/lib/imperium.rb
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
require 'imperium/error'
|
2
|
+
|
3
|
+
require 'imperium/configuration'
|
4
|
+
require 'imperium/client'
|
5
|
+
require 'imperium/http_client'
|
6
|
+
require 'imperium/kv'
|
7
|
+
require 'imperium/kv_pair'
|
8
|
+
require 'imperium/kv_get_response'
|
9
|
+
require 'imperium/response'
|
10
|
+
require 'imperium/version'
|
11
|
+
|
12
|
+
module Imperium
|
13
|
+
def self.configure
|
14
|
+
yield configuration
|
15
|
+
ensure
|
16
|
+
Client.reset_default_clients
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.configuration
|
20
|
+
@configuration ||= Configuration.new
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
require 'base64'
|
2
|
+
require 'json'
|
3
|
+
|
4
|
+
module Imperium
|
5
|
+
class Client
|
6
|
+
class << self
|
7
|
+
attr_reader :subclasses
|
8
|
+
attr_accessor :path_prefix
|
9
|
+
|
10
|
+
def default_client
|
11
|
+
@default_client ||= new(Imperium.configuration)
|
12
|
+
end
|
13
|
+
|
14
|
+
def reset_default_client
|
15
|
+
@default_client = nil
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
@subclasses = []
|
20
|
+
def self.inherited(subclass)
|
21
|
+
@subclasses << subclass
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.reset_default_clients
|
25
|
+
@subclasses.each(&:reset_default_client)
|
26
|
+
end
|
27
|
+
|
28
|
+
attr_reader :config
|
29
|
+
|
30
|
+
def initialize(config)
|
31
|
+
@config = config
|
32
|
+
@http_client = Imperium::HTTPClient.new(config)
|
33
|
+
end
|
34
|
+
|
35
|
+
def path_prefix
|
36
|
+
self.class.path_prefix
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
def extract_query_params(full_options, allowed_params: :all)
|
42
|
+
if full_options.key?(:consistent) && full_options.key?(:stale)
|
43
|
+
raise InvalidConsistencySpecification, 'Both consistency modes (consistent, stale) supplied, this is not allowed by the HTTP API'
|
44
|
+
end
|
45
|
+
allowed_params == :all ? full_options : full_options.select { |k, _| allowed_params.include?(k.to_sym) }
|
46
|
+
end
|
47
|
+
|
48
|
+
def hashify_options(options_array)
|
49
|
+
options_array.inject({}) { |hash, value|
|
50
|
+
value.is_a?(Hash) ? hash.merge(value) : hash.merge(value.to_sym => nil)
|
51
|
+
}
|
52
|
+
end
|
53
|
+
|
54
|
+
def prefix_path(main_path, prefix = self.path_prefix)
|
55
|
+
"#{prefix}/#{main_path}"
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
require 'addressable/uri'
|
2
|
+
require 'forwardable'
|
3
|
+
|
4
|
+
module Imperium
|
5
|
+
# The Configuration class represents the values necessary for making contact
|
6
|
+
# with a Consul agent.
|
7
|
+
#
|
8
|
+
# @!attribute [rw] connect_timeout
|
9
|
+
# @return [Integer] The number of seconds to wait for a connection to Consul
|
10
|
+
# to open before failing, default: 5
|
11
|
+
# @!attribute [rw] receive_timeout
|
12
|
+
# @return [Integer] The number of seconds to wait for a response from Consul
|
13
|
+
# to open before failing, default: 60. This default is quite high in order to
|
14
|
+
# support long polling.
|
15
|
+
# @!attribute [rw] send_timeout
|
16
|
+
# @return [Integer] The number of seconds to wait for the request body to
|
17
|
+
# finish uploading to Consul to open before failing, default: 15.
|
18
|
+
# @!attribute [rw] token
|
19
|
+
# @return [String] The token to be used when making requests to the Consul
|
20
|
+
# APIs. Defaults to `nil`
|
21
|
+
# @!attribute [rw] url
|
22
|
+
# @return [Addressable::URI] The base URL, including port, for contacting
|
23
|
+
# the Consul agent. Defaults to `http://localhost:8500`
|
24
|
+
class Configuration
|
25
|
+
extend Forwardable
|
26
|
+
|
27
|
+
attr_reader :url
|
28
|
+
attr_accessor :connect_timeout, :receive_timeout, :send_timeout, :token
|
29
|
+
|
30
|
+
def initialize(url: 'http://localhost:8500', token: nil)
|
31
|
+
@url = Addressable::URI.parse(url)
|
32
|
+
@connect_timeout = 5
|
33
|
+
@send_timeout = 15
|
34
|
+
@receive_timeout = 60
|
35
|
+
@token = token
|
36
|
+
end
|
37
|
+
|
38
|
+
def_delegators :@url, :host, :host=, :port, :port=
|
39
|
+
|
40
|
+
# Check if the specified URL is using SSL/TLS
|
41
|
+
# @return [Boolean]
|
42
|
+
def ssl?
|
43
|
+
@url.scheme == 'https'
|
44
|
+
end
|
45
|
+
|
46
|
+
# Configure the clients to use SSL/TLS (or not).
|
47
|
+
#
|
48
|
+
# @param value [Boolean]
|
49
|
+
# @raise [NoMethodError] When the URL has previously been set to nil.
|
50
|
+
def ssl=(value)
|
51
|
+
@url.scheme = (!!value ? 'https' : 'http')
|
52
|
+
end
|
53
|
+
|
54
|
+
# Check for the presence of a token
|
55
|
+
# @return [Boolean]
|
56
|
+
def token?
|
57
|
+
@token && !@token.empty?
|
58
|
+
end
|
59
|
+
|
60
|
+
# Set the URL
|
61
|
+
#
|
62
|
+
# This method will append a trailing slash to the supplied URL if not
|
63
|
+
# included. We're doing this because merging a path onto a URL missing the
|
64
|
+
# trailing slash will remove any extant path components.
|
65
|
+
#
|
66
|
+
# @param value [String, Addressable::URI, URI::GenericURI] The new value to use.
|
67
|
+
def url=(value)
|
68
|
+
if value.nil?
|
69
|
+
@url = nil
|
70
|
+
else
|
71
|
+
@url = Addressable::URI.parse(value)
|
72
|
+
@url.path << '/' unless @url.path.end_with?('/')
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
require 'httpclient'
|
2
|
+
|
3
|
+
module Imperium
|
4
|
+
class HTTPClient
|
5
|
+
attr_reader :config
|
6
|
+
|
7
|
+
def initialize(config)
|
8
|
+
@config = config
|
9
|
+
@driver = ::HTTPClient.new
|
10
|
+
@driver.connect_timeout = @config.connect_timeout
|
11
|
+
@driver.send_timeout = @config.send_timeout
|
12
|
+
@driver.receive_timeout = @config.receive_timeout
|
13
|
+
end
|
14
|
+
|
15
|
+
def delete(path)
|
16
|
+
url = config.url.join(path)
|
17
|
+
@driver.delete(url)
|
18
|
+
end
|
19
|
+
|
20
|
+
def get(path, query: {})
|
21
|
+
url = config.url.join(path)
|
22
|
+
url.query_values = query
|
23
|
+
@driver.get(url, header: build_request_headers)
|
24
|
+
end
|
25
|
+
|
26
|
+
def put(path, value)
|
27
|
+
url = config.url.join(path)
|
28
|
+
@driver.put(url, body: value)
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def build_request_headers
|
34
|
+
if config.token?
|
35
|
+
{'X-Consul-Token' => config.token}
|
36
|
+
else
|
37
|
+
{}
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
data/lib/imperium/kv.rb
ADDED
@@ -0,0 +1,92 @@
|
|
1
|
+
module Imperium
|
2
|
+
# A client for the KV API.
|
3
|
+
class KV < Client
|
4
|
+
self.path_prefix = 'v1/kv'.freeze
|
5
|
+
|
6
|
+
# {#get GET} a key using the {.default_client}
|
7
|
+
# @see #get
|
8
|
+
def self.get(key, *options)
|
9
|
+
default_client.get(key, *options)
|
10
|
+
end
|
11
|
+
|
12
|
+
# Delete the specified key
|
13
|
+
# @note This is really a stub of this method, it will delete the key but
|
14
|
+
# you'll get back a raw
|
15
|
+
# {http://www.rubydoc.info/gems/httpclient/HTTP/Message HTTP::Message}
|
16
|
+
# object. If you're really serious about using this we'll probably want
|
17
|
+
# to build a wrapper around the response with some logic to simplify
|
18
|
+
# interpreting the response.
|
19
|
+
#
|
20
|
+
# @param key [String] The key to be deleted
|
21
|
+
# @param options [Array] Un-used, only here to prevent changing the method
|
22
|
+
# signature when we actually implement more advanced functionality.
|
23
|
+
# @return [HTTP::Message]
|
24
|
+
def delete(key, *options)
|
25
|
+
@http_client.delete(prefix_path(key))
|
26
|
+
end
|
27
|
+
|
28
|
+
GET_ALLOWED_OPTIONS = %i{consistent stale recurse keys separator raw}.freeze
|
29
|
+
private_constant :GET_ALLOWED_OPTIONS
|
30
|
+
# Get the specified key/prefix using the supplied options.
|
31
|
+
#
|
32
|
+
# @example Fetching a key that is allowed to be stale.
|
33
|
+
# response = Imperium::KV.get('foo/bar', :stale) # => KVGETResponse...
|
34
|
+
#
|
35
|
+
# @example Fetching a prefix recursively allowing values to be stale.
|
36
|
+
# response = Imperium::KV.get('foo/bar', :stale, :recurse) # => KVGETResponse...
|
37
|
+
#
|
38
|
+
# @todo Support blocking queries by accepting an :index parameter
|
39
|
+
#
|
40
|
+
# @param [String] key The key/prefix to be fetched from Consul.
|
41
|
+
# @param [Array<Symbol,String,Hash>] options The options for constructing
|
42
|
+
# the request
|
43
|
+
# @option options [Symbol] :consistent Specify the consistent option to the
|
44
|
+
# API resulting in the most up to date value possible at the expense of a
|
45
|
+
# bit of latency and the requirement of a validly elected leader. See
|
46
|
+
# {https://www.consul.io/docs/agent/http.html#consistency-modes Consistency Modes documentation}.
|
47
|
+
# @option options [Symbol] :stale Specify the stale option to the API
|
48
|
+
# resulting in a potentially stale value with the benefit of a faster,
|
49
|
+
# more scaleable read. See
|
50
|
+
# {https://www.consul.io/docs/agent/http.html#consistency-modes Consistency Modes documentation}.
|
51
|
+
# @option options [Symbol] :recurse Supply the recurse option to the API to
|
52
|
+
# fetch any keys with the specified prefix.
|
53
|
+
# @option options [Symbol] :keys Fetch only the keys with the specified prefix.
|
54
|
+
# @option options [String] :separator See
|
55
|
+
# {https://www.consul.io/docs/agent/http/kv.html#get-method Consul's Documentation}
|
56
|
+
# @return [KVGETResponse]
|
57
|
+
def get(key, *options)
|
58
|
+
expanded_options = hashify_options(options)
|
59
|
+
query_params = extract_query_params(expanded_options, allowed_params: GET_ALLOWED_OPTIONS)
|
60
|
+
response = @http_client.get(prefix_path(key), query: query_params)
|
61
|
+
KVGETResponse.new(response, prefix: key, options: expanded_options)
|
62
|
+
end
|
63
|
+
|
64
|
+
# Update or create the specified key
|
65
|
+
# @note This is really a stub of this method, it will put the key but
|
66
|
+
# you'll get back a raw
|
67
|
+
# {http://www.rubydoc.info/gems/httpclient/HTTP/Message HTTP::Message}
|
68
|
+
# object. If you're really serious about using this we'll probably want
|
69
|
+
# to build a wrapper around the response with some logic to simplify
|
70
|
+
# interpreting the response.
|
71
|
+
#
|
72
|
+
# @param key [String] The key to be created or updated.
|
73
|
+
# @param value [String] The value to be set on the key.
|
74
|
+
# @param options [Array] Un-used, only here to prevent changing the method
|
75
|
+
# signature when we actually implement more advanced functionality.
|
76
|
+
# @return [HTTP::Message]
|
77
|
+
def put(key, value, *options)
|
78
|
+
@http_client.put(prefix_path(key), value)
|
79
|
+
end
|
80
|
+
|
81
|
+
private
|
82
|
+
|
83
|
+
def construct_nested_hash(key_parts, value)
|
84
|
+
key = key_parts.shift
|
85
|
+
if key_parts.empty?
|
86
|
+
{key => value}
|
87
|
+
else
|
88
|
+
{key => construct_nested_hash(key_parts, value)}
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
@@ -0,0 +1,102 @@
|
|
1
|
+
require_relative 'response'
|
2
|
+
|
3
|
+
module Imperium
|
4
|
+
# KVGETResponse is a wrapper for the raw HTTP::Message response from the API
|
5
|
+
#
|
6
|
+
# @note This class doesn't really make sense to be instantiated outside of
|
7
|
+
# {KV#get}
|
8
|
+
#
|
9
|
+
# @!attribute [rw] options
|
10
|
+
# @return [Hash<Symbol, Object>] The options for the get request after being
|
11
|
+
# coerced from an array to hash.
|
12
|
+
# @attribute [rw] prefix
|
13
|
+
# @return [String] The key prefix requested from the api, used to coerce the
|
14
|
+
# returned values from the API into their various shapes.
|
15
|
+
class KVGETResponse < Response
|
16
|
+
attr_accessor :options, :prefix
|
17
|
+
|
18
|
+
def initialize(message, options: {}, prefix: '')
|
19
|
+
super message
|
20
|
+
@prefix = prefix
|
21
|
+
@options = options
|
22
|
+
end
|
23
|
+
|
24
|
+
# Construct an Array of KV pairs from a response, including their full
|
25
|
+
# metadata.
|
26
|
+
#
|
27
|
+
# @return [nil] When the keys option was supplied.
|
28
|
+
# @return [Array<KVPair>] When there are values present, and an empty array
|
29
|
+
# when the response is a 404.
|
30
|
+
def found_objects
|
31
|
+
return if options.key?(:keys)
|
32
|
+
return [] if not_found?
|
33
|
+
@found_objects ||= parsed_body.map { |attrs| KVPair.new(attrs) }
|
34
|
+
end
|
35
|
+
|
36
|
+
def prefix=(value)
|
37
|
+
@prefix = (value.nil? ? nil : value.sub(/\/\z/, ''))
|
38
|
+
end
|
39
|
+
|
40
|
+
MERGING_FUNC = -> (_, old, new) {
|
41
|
+
if old.is_a?(Hash) && new.is_a?(Hash)
|
42
|
+
old.merge(new, &MERGING_FUNC)
|
43
|
+
else
|
44
|
+
new
|
45
|
+
end
|
46
|
+
}
|
47
|
+
private_constant :MERGING_FUNC
|
48
|
+
|
49
|
+
# Extracts the values from the response and smashes them into a simple
|
50
|
+
# object depending on options provided on the request.
|
51
|
+
#
|
52
|
+
# @example A nested hash constructed from recursively found values
|
53
|
+
# # Given a response including the following values (metadata ommitted for clarity):
|
54
|
+
# # [
|
55
|
+
# # {"Key" => "foo/bar/baz/first", "Value" => "cXV4Cg=="},
|
56
|
+
# # {"Key" => "foo/bar/baz/second/deep", "Value" => "cHVycGxlCg=="}
|
57
|
+
# # ]
|
58
|
+
# response = Imperium::KV.get('foo/bar/baz', :recurse)
|
59
|
+
# response.values # => {'first' => 'qux', 'second' => {'deep' => 'purple'}}
|
60
|
+
#
|
61
|
+
# @return [String] When the matching key is found without the `recurse`
|
62
|
+
# option as well as when a single value is found with the recurse option
|
63
|
+
# and the key exactly matches the prefix.
|
64
|
+
# @return [Hash{String => Hash,String}] When the recurse option is included
|
65
|
+
# and there are keys present nested within the prefix.
|
66
|
+
# @return [Array<String>] An array of strings representing all of the keys
|
67
|
+
# within the specified prefix when the keys option is included.
|
68
|
+
# @return [nil] When the response status code is 404 (Not Found)
|
69
|
+
def values
|
70
|
+
return if not_found?
|
71
|
+
return parsed_body if options.key?(:keys)
|
72
|
+
if options.key?(:recurse)
|
73
|
+
if found_objects.size == 1 && found_objects.first.key == prefix
|
74
|
+
found_objects.first.value
|
75
|
+
else
|
76
|
+
found_objects.inject({}) do |hash, obj|
|
77
|
+
if prefix.empty?
|
78
|
+
unprefixed_key = obj.key
|
79
|
+
else
|
80
|
+
unprefixed_key = obj.key[prefix.length + 1..-1]
|
81
|
+
end
|
82
|
+
key_parts = unprefixed_key.split('/')
|
83
|
+
hash.merge(construct_nested_hash(key_parts, obj.value), &MERGING_FUNC)
|
84
|
+
end
|
85
|
+
end
|
86
|
+
else
|
87
|
+
found_objects.first.value
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
private
|
92
|
+
|
93
|
+
def construct_nested_hash(key_parts, value)
|
94
|
+
key = key_parts.shift
|
95
|
+
if key_parts.empty?
|
96
|
+
{key => value}
|
97
|
+
else
|
98
|
+
{key => construct_nested_hash(key_parts, value)}
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
require 'base64'
|
2
|
+
|
3
|
+
module Imperium
|
4
|
+
# KVPair provides a more OO/Rubyish interface to the objects returned from
|
5
|
+
# the KV API on a GET request.
|
6
|
+
#
|
7
|
+
# @see https://www.consul.io/docs/agent/http/kv.html#get-method Consul KV GET Documentation
|
8
|
+
#
|
9
|
+
# @!attribute [rw] lock_index
|
10
|
+
# @return [Integer] The number of times this key has successfully been
|
11
|
+
# locked, the {#session} attribute indicates which session owns the lock.
|
12
|
+
# @!attribute [rw] session
|
13
|
+
# @return [String] The identifier for the session that owns the lock.
|
14
|
+
# @!attribute [rw] key
|
15
|
+
# @return [String] The full path for the entry.
|
16
|
+
# @!attribute [rw] flags
|
17
|
+
# @return [Integer] An opaque unsigned integer for use by the client
|
18
|
+
# application.
|
19
|
+
# @!attribute [rw] value
|
20
|
+
# @return [String] The stored value (returned already base64 decoded)
|
21
|
+
# @!attribute [rw] create_index
|
22
|
+
# @return [Integer] The internal index value representing when the entry
|
23
|
+
# was created.
|
24
|
+
# @!attribute [rw] modify_index
|
25
|
+
# @return [Integer] The internal index value representing when the entry
|
26
|
+
# was last updated.
|
27
|
+
class KVPair
|
28
|
+
ATTRIBUTE_MAP = {
|
29
|
+
'LockIndex' => :lock_index,
|
30
|
+
'Session' => :session,
|
31
|
+
'Key' => :key,
|
32
|
+
'Flags' => :flags,
|
33
|
+
'Value' => :value,
|
34
|
+
'CreateIndex' => :create_index,
|
35
|
+
'ModifyIndex' => :modify_index,
|
36
|
+
}.freeze
|
37
|
+
private_constant :ATTRIBUTE_MAP
|
38
|
+
|
39
|
+
ATTRIBUTE_NAMES = ATTRIBUTE_MAP.values
|
40
|
+
private_constant :ATTRIBUTE_NAMES
|
41
|
+
|
42
|
+
attr_accessor *ATTRIBUTE_NAMES
|
43
|
+
|
44
|
+
# Initialize a {KVPair}
|
45
|
+
#
|
46
|
+
# @param attributes [Hash] The attributes for this object as parsed from the
|
47
|
+
# API response.
|
48
|
+
def initialize(attributes = {})
|
49
|
+
ATTRIBUTE_MAP.each do |key, attribute_name|
|
50
|
+
send("#{attribute_name}=", attributes[key])
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def ==(other)
|
55
|
+
return false unless self.class === other
|
56
|
+
ATTRIBUTE_NAMES.all? { |attr| self.send(attr) == other.send(attr )}
|
57
|
+
end
|
58
|
+
|
59
|
+
# Capture and base64 decode a value from the api.
|
60
|
+
#
|
61
|
+
# @param value [String] The base64 encoded value from the response.
|
62
|
+
def value=(value)
|
63
|
+
@value = Base64.decode64 value
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
require 'delegate'
|
2
|
+
|
3
|
+
module Imperium
|
4
|
+
# A Response is a decorator around the
|
5
|
+
# {http://www.rubydoc.info/gems/httpclient/HTTP/Message HTTP::Message} object
|
6
|
+
# returned when a request is made.
|
7
|
+
#
|
8
|
+
# It exposes, through a convenient API, headers common to all interactions
|
9
|
+
# with the Consul HTTP API
|
10
|
+
class Response < SimpleDelegator
|
11
|
+
# Indicates if the contacted server has a known leader.
|
12
|
+
#
|
13
|
+
# @return [TrueClass] When the response indicates there is a known leader
|
14
|
+
# @return [FalseClass] When the response indicates there is not a known leader
|
15
|
+
# @return [NilClass] When the X-Consul-KnownLeader header is not present.
|
16
|
+
def known_leader?
|
17
|
+
return unless headers.key?('X-Consul-KnownLeader')
|
18
|
+
headers['X-Consul-KnownLeader'] == 'true'
|
19
|
+
end
|
20
|
+
|
21
|
+
# The time in miliseconds since the contacted server has been in contact
|
22
|
+
# with the leader.
|
23
|
+
#
|
24
|
+
# @return [NilClass] When the X-Consul-LastContact header is not present.
|
25
|
+
# @return [Integer]
|
26
|
+
def last_contact
|
27
|
+
return unless headers.key?('X-Consul-LastContact')
|
28
|
+
Integer(headers['X-Consul-LastContact'])
|
29
|
+
end
|
30
|
+
|
31
|
+
# A convenience method for checking if the response had a 404 status code.
|
32
|
+
def not_found?
|
33
|
+
status == 404
|
34
|
+
end
|
35
|
+
|
36
|
+
# Indicate status of translate_wan_addrs setting on the server.
|
37
|
+
#
|
38
|
+
# @return [TrueClass] When X-Consul-Translate-Addresses is set
|
39
|
+
# @return [FalseClass] When X-Consul-Translate-Addresses is unset
|
40
|
+
def translate_addresses?
|
41
|
+
headers.key?('X-Consul-Translate-Addresses')
|
42
|
+
end
|
43
|
+
|
44
|
+
private
|
45
|
+
|
46
|
+
def parsed_body
|
47
|
+
JSON.parse(content)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
metadata
ADDED
@@ -0,0 +1,193 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: imperium
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Tyler Pickett
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2017-03-29 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: addressable
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 2.5.0
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 2.5.0
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: httpclient
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '2.8'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '2.8'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: bundler
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '1.13'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '1.13'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: byebug
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: rake
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '10.0'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '10.0'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: rspec
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - "~>"
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '3.0'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - "~>"
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '3.0'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: webmock
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - "~>"
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: 2.3.2
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - "~>"
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: 2.3.2
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: wwtd
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - "~>"
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '1.3'
|
118
|
+
type: :development
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - "~>"
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: '1.3'
|
125
|
+
- !ruby/object:Gem::Dependency
|
126
|
+
name: yard
|
127
|
+
requirement: !ruby/object:Gem::Requirement
|
128
|
+
requirements:
|
129
|
+
- - ">="
|
130
|
+
- !ruby/object:Gem::Version
|
131
|
+
version: '0'
|
132
|
+
type: :development
|
133
|
+
prerelease: false
|
134
|
+
version_requirements: !ruby/object:Gem::Requirement
|
135
|
+
requirements:
|
136
|
+
- - ">="
|
137
|
+
- !ruby/object:Gem::Version
|
138
|
+
version: '0'
|
139
|
+
description: A powerful, easy to use, Consul client
|
140
|
+
email:
|
141
|
+
- t.pickett66@gmail.com
|
142
|
+
executables: []
|
143
|
+
extensions: []
|
144
|
+
extra_rdoc_files: []
|
145
|
+
files:
|
146
|
+
- ".gitignore"
|
147
|
+
- ".travis.yml"
|
148
|
+
- Dockerfile.ci
|
149
|
+
- Gemfile
|
150
|
+
- README.md
|
151
|
+
- Rakefile
|
152
|
+
- bin/console
|
153
|
+
- bin/rspec
|
154
|
+
- bin/setup
|
155
|
+
- build.sh
|
156
|
+
- docker-compose.override.yml.example
|
157
|
+
- docker-compose.yml
|
158
|
+
- imperium.gemspec
|
159
|
+
- lib/imperium.rb
|
160
|
+
- lib/imperium/client.rb
|
161
|
+
- lib/imperium/configuration.rb
|
162
|
+
- lib/imperium/error.rb
|
163
|
+
- lib/imperium/http_client.rb
|
164
|
+
- lib/imperium/kv.rb
|
165
|
+
- lib/imperium/kv_get_response.rb
|
166
|
+
- lib/imperium/kv_pair.rb
|
167
|
+
- lib/imperium/response.rb
|
168
|
+
- lib/imperium/version.rb
|
169
|
+
homepage: https://github.com/instructure/imperium
|
170
|
+
licenses:
|
171
|
+
- MIT
|
172
|
+
metadata: {}
|
173
|
+
post_install_message:
|
174
|
+
rdoc_options: []
|
175
|
+
require_paths:
|
176
|
+
- lib
|
177
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
178
|
+
requirements:
|
179
|
+
- - ">="
|
180
|
+
- !ruby/object:Gem::Version
|
181
|
+
version: '0'
|
182
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
183
|
+
requirements:
|
184
|
+
- - ">="
|
185
|
+
- !ruby/object:Gem::Version
|
186
|
+
version: '0'
|
187
|
+
requirements: []
|
188
|
+
rubyforge_project:
|
189
|
+
rubygems_version: 2.6.8
|
190
|
+
signing_key:
|
191
|
+
specification_version: 4
|
192
|
+
summary: A powerful, easy to use, Consul client
|
193
|
+
test_files: []
|