imperium 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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: []