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.
@@ -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
@@ -0,0 +1,11 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+
11
+ docker-compose.override.yml
@@ -0,0 +1,8 @@
1
+ sudo: false
2
+ language: ruby
3
+ before_install: gem install bundler -v 1.13.6
4
+ rvm:
5
+ - "2.1"
6
+ - "2.2"
7
+ - "2.3"
8
+ - "2.4.0"
@@ -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
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in imperium.gemspec
4
+ gemspec
@@ -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.
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
@@ -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
@@ -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")
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,5 @@
1
+ #!/bin/bash
2
+ set -e
3
+
4
+ docker-compose build
5
+ docker-compose run --rm app $@
@@ -0,0 +1,9 @@
1
+ version: "2"
2
+ services:
3
+ app:
4
+ volumes:
5
+ - "gems:/home/docker/.gem"
6
+ - ".:/app"
7
+
8
+ volumes:
9
+ gems: {}
@@ -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
@@ -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
@@ -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,5 @@
1
+
2
+ module Imperium
3
+ class Error < StandardError; end
4
+ class InvalidConsistencySpecification < Error; end
5
+ 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
@@ -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
@@ -0,0 +1,3 @@
1
+ module Imperium
2
+ VERSION = "0.1.0"
3
+ 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: []