class Diplomat::RestClient
Base class for interacting with the Consul RESTful API
Public Class Methods
# File lib/diplomat/rest_client.rb, line 46 def access_method?(meth_id) @access_methods.include? meth_id end
Allow certain methods to be accessed without defining “new”. @param meth_id [Symbol] symbol defining method requested @param *args Arguments list @return [Boolean]
# File lib/diplomat/rest_client.rb, line 55 def method_missing(meth_id, *args) if access_method?(meth_id) new.send(meth_id, *args) else # See https://bugs.ruby-lang.org/issues/10969 begin super rescue NameError => e raise NoMethodError, e end end end
Initialize the fadaray connection @param api_connection [Faraday::Connection,nil] supply mock API Connection @param configuration [Diplomat::Configuration] a dedicated config to use
# File lib/diplomat/rest_client.rb, line 14 def initialize(api_connection = nil, configuration: nil) @configuration = configuration start_connection api_connection end
Make ‘respond_to?` aware of method short-cuts.
@param meth_id [Symbol] the tested method @oaram with_private if private methods should be tested too
# File lib/diplomat/rest_client.rb, line 73 def respond_to?(meth_id, with_private = false) access_method?(meth_id) || super end
Make ‘respond_to_missing` aware of method short-cuts. This is needed for {#method} to work on these, which is helpful for testing purposes.
@param meth_id [Symbol] the tested method @oaram with_private if private methods should be tested too
# File lib/diplomat/rest_client.rb, line 82 def respond_to_missing?(meth_id, with_private = false) access_method?(meth_id) || super end
Public Instance Methods
Assemble a url from an array of parts. @param parts [Array] the url chunks to be assembled @return [String] the resultant url string
# File lib/diplomat/rest_client.rb, line 36 def concat_url(parts) parts.reject!(&:empty?) if parts.length > 1 parts.first + '?' + parts.drop(1).join('&') else parts.first end end
Get client configuration or global one if not specified via initialize. @return [Diplomat::Configuration] used by this client
# File lib/diplomat/rest_client.rb, line 21 def configuration @configuration || ::Diplomat.configuration end
Format url parameters into strings correctly @param name [String] the name of the parameter @param value [String] the value of the parameter @return [Array] the resultant parameter string inside an array.
# File lib/diplomat/rest_client.rb, line 29 def use_named_parameter(name, value) value ? ["#{name}=#{value}"] : [] end
Protected Instance Methods
Turn the given key into something that the Consul API will consider its canonical form. If we don’t do this, then the Consul API will return a HTTP 301 response directing us to the same action with a canonicalized key, and we’d have to waste time following that redirect.
# File lib/diplomat/rest_client.rb, line 94 def normalize_key_for_uri(key) # The Consul docs suggest using slashes to organise keys # (https://www.consul.io/docs/agent/kv.html#using-consul-kv). # # However, Consul (like many servers) does strange things with slashes, # presumably to "paper over" users' errors in typing URLs. # E.g. the key "/my/path" will end up in the URI path component # "/v1/kv//my/path", which Consul will redirect (HTTP 301) to # "/v1/kv/my/path" -- a very different URI! # # One solution might be to simply always URI-encode slashes # (and all other non-URI-safe characters), but that appears to # result in some other weirdness, e.g., keys being returned with # URI-encoding in them in contexts totally unrelated to URIs. # For examples, see these issues and follow the links: # # - https://github.com/hashicorp/consul/issues/889 # - https://github.com/hashicorp/consul/issues/1277 # # For now it seems safest to simply assume that leading literal # slashes on keys are benign mistakes, and strip them off. # Hopefully the expected behaviour will be formalised/clarified # in future versions of Consul, and we can introduce some stricter # and more predictable handling of keys on this side. if key.start_with? '/' key[1..-1] else key.freeze end end
Private Instance Methods
# File lib/diplomat/rest_client.rb, line 134 def build_connection(api_connection, raise_error = false) api_connection || Faraday.new(configuration.url, configuration.options) do |faraday| configuration.middleware.each do |middleware| faraday.use middleware end faraday.request :url_encoded faraday.response :raise_error unless raise_error # We have to provide a custom params encoder here because Faraday - by default - assumes that # list keys have [] as part of their name. This is however does not match the expectation of # the Consul API, which assumes the same query param to simply be repeated # # So faraday reduces this: http://localhost:8500?a=1&a=2 to http://localhost:8500?a=2 unless you # explicitly tell it not to. faraday.options[:params_encoder] = Faraday::FlatParamsEncoder faraday.adapter Faraday.default_adapter end end
Converts k/v data into ruby hash
# File lib/diplomat/rest_client.rb, line 156 def convert_to_hash(data) data_h = data.map do |item| item[:key].split('/').reverse.reduce(item[:value]) { |h, v| { v => h } } end data_h.reduce({}) do |dest, source| DeepMerge.deep_merge!(source, dest, { preserve_unmergeables: true }) end end
Return @raw with Value fields decoded
# File lib/diplomat/rest_client.rb, line 173 def decode_values return @raw if @raw.first.is_a? String @raw.each_with_object([]) do |acc, el| begin acc['Value'] = Base64.decode64(acc['Value']) rescue StandardError nil end el << acc el end end
Parse the body, apply it to the raw attribute
# File lib/diplomat/rest_client.rb, line 166 def parse_body return JSON.parse(@raw.body) if @raw.status == 200 raise Diplomat::UnknownStatus, "status #{@raw.status}: #{@raw.body}" end
rubocop:disable Metrics/PerceivedComplexity TODO: Migrate all custom params in options
# File lib/diplomat/rest_client.rb, line 224 def parse_options(options) headers = nil query_params = [] url_prefix = nil consistency = [] # Parse options used as header headers = { 'X-Consul-Token' => configuration.acl_token } if configuration.acl_token headers = { 'X-Consul-Token' => options[:token] } if options[:token] # Parse consistency options used as query params consistency = 'stale' if options[:stale] consistency = 'leader' if options[:leader] consistency = 'consistent' if options[:consistent] query_params << consistency query_params << 'cached' if options[:cached] # Parse url host url_prefix = options[:http_addr] if options[:http_addr] { query_params: query_params, headers: headers, url_prefix: url_prefix } end
Get the name and payload(s) from the raw output
# File lib/diplomat/rest_client.rb, line 207 def return_payload @value = @raw.map do |e| { name: e['Name'], payload: (Base64.decode64(e['Payload']) unless e['Payload'].nil?) } end end
Get the key/value(s) from the raw output rubocop:disable Metrics/PerceivedComplexity
# File lib/diplomat/rest_client.rb, line 189 def return_value(nil_values = false, transformation = nil, return_hash = false) @value = decode_values return @value if @value.first.is_a? String if @value.count == 1 && !return_hash @value = @value.first['Value'] @value = transformation.call(@value) if transformation && !@value.nil? return @value else @value = @value.map do |el| el['Value'] = transformation.call(el['Value']) if transformation && !el['Value'].nil? { key: el['Key'], value: el['Value'] } if el['Value'] || nil_values end.compact end end
# File lib/diplomat/rest_client.rb, line 297 def send_delete_request(connection, url, options, custom_params = nil) rest_options = parse_options(options) url += rest_options[:query_params] url += custom_params unless custom_params.nil? connection.delete do |req| req.url rest_options[:url_prefix] ? rest_options[:url_prefix] + concat_url(url) : concat_url(url) rest_options[:headers].map { |k, v| req.headers[k.to_sym] = v } unless rest_options[:headers].nil? end end
rubocop:enable Metrics/PerceivedComplexity
# File lib/diplomat/rest_client.rb, line 248 def send_get_request(connection, url, options, custom_params = nil) rest_options = parse_options(options) url += rest_options[:query_params] url += custom_params unless custom_params.nil? begin connection.get do |req| req.options[:params_encoder] = options[:params_encoder] if options[:params_encoder] req.url rest_options[:url_prefix] ? rest_options[:url_prefix] + concat_url(url) : concat_url(url) rest_options[:headers].map { |k, v| req.headers[k.to_sym] = v } unless rest_options[:headers].nil? req.options.timeout = options[:timeout] if options[:timeout] end rescue Faraday::ClientError, Faraday::ServerError => e resp = e.response if resp raise Diplomat::AclNotFound, e if resp[:status] == 403 && resp[:body] == 'ACL not found' end raise Diplomat::PathNotFound, e end end
# File lib/diplomat/rest_client.rb, line 286 def send_post_request(connection, url, options, data, custom_params = nil) rest_options = parse_options(options) url += rest_options[:query_params] url += custom_params unless custom_params.nil? connection.post do |req| req.url rest_options[:url_prefix] ? rest_options[:url_prefix] + concat_url(url) : concat_url(url) rest_options[:headers].map { |k, v| req.headers[k.to_sym] = v } unless rest_options[:headers].nil? req.body = JSON.dump(data) unless data.nil? end end
# File lib/diplomat/rest_client.rb, line 268 def send_put_request(connection, url, options, data, custom_params = nil, mime = 'application/json') rest_options = parse_options(options) url += rest_options[:query_params] url += custom_params unless custom_params.nil? connection.put do |req| req.url rest_options[:url_prefix] ? rest_options[:url_prefix] + concat_url(url) : concat_url(url) rest_options[:headers].map { |k, v| req.headers[k.to_sym] = v } unless rest_options[:headers].nil? unless data.nil? (req.headers || {})['Content-Type'] = mime req.body = if mime == 'application/json' && !data.is_a?(String) data.to_json else data end end end end
Build the API Client @param api_connection [Faraday::Connection,nil] supply mock API Connection
# File lib/diplomat/rest_client.rb, line 129 def start_connection(api_connection = nil) @conn = build_connection(api_connection) @conn_no_err = build_connection(api_connection, true) end
# File lib/diplomat/rest_client.rb, line 214 def use_cas(options) options ? use_named_parameter('cas', options[:cas]) : [] end
# File lib/diplomat/rest_client.rb, line 218 def use_consistency(options) options[:consistency] ? [options[:consistency].to_s] : [] end
Mapping for valid key/value store transaction verbs and required parameters
@return [Hash] valid key/store transaction verbs and required parameters
# File lib/diplomat/rest_client.rb, line 310 def valid_transaction_verbs { 'set' => %w[Key Value], 'cas' => %w[Key Value Index], 'lock' => %w[Key Value Session], 'unlock' => %w[Key Value Session], 'get' => %w[Key], 'get-tree' => %w[Key], 'check-index' => %w[Key Index], 'check-session' => %w[Key Session], 'delete' => %w[Key], 'delete-tree' => %w[Key], 'delete-cas' => %w[Key Index] } end
Key/value store transactions that require that a value be set
@return [Array<String>] verbs that require a value be set
# File lib/diplomat/rest_client.rb, line 329 def valid_value_transactions @valid_value_transactions ||= valid_transaction_verbs.select do |verb, requires| verb if requires.include? 'Value' end end