class Diplomat::RestClient

Base class for interacting with the Consul RESTful API

Public Class Methods

access_method?(meth_id) click to toggle source
# File lib/diplomat/rest_client.rb, line 46
def access_method?(meth_id)
  @access_methods.include? meth_id
end
method_missing(meth_id, *args) click to toggle source

Allow certain methods to be accessed without defining “new”. @param meth_id [Symbol] symbol defining method requested @param *args Arguments list @return [Boolean]

Calls superclass method
# 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
new(api_connection = nil, configuration: nil) click to toggle source

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
respond_to?(meth_id, with_private = false) click to toggle source

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

Calls superclass method
# File lib/diplomat/rest_client.rb, line 73
def respond_to?(meth_id, with_private = false)
  access_method?(meth_id) || super
end
respond_to_missing?(meth_id, with_private = false) click to toggle source

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

Calls superclass method
# 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

concat_url(parts) click to toggle source

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
configuration() click to toggle source

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
use_named_parameter(name, value) click to toggle source

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

normalize_key_for_uri(key) click to toggle source

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

build_connection(api_connection, raise_error = false) click to toggle source
# 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
convert_to_hash(data) click to toggle source

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
decode_values() click to toggle source

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_body() click to toggle source

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
parse_options(options) click to toggle source

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
return_payload() click to toggle source

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
return_value(nil_values = false, transformation = nil, return_hash = false) click to toggle source

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
send_delete_request(connection, url, options, custom_params = nil) click to toggle source
# 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
send_get_request(connection, url, options, custom_params = nil) click to toggle source

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
send_post_request(connection, url, options, data, custom_params = nil) click to toggle source
# 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
send_put_request(connection, url, options, data, custom_params = nil, mime = 'application/json') click to toggle source
# 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
start_connection(api_connection = nil) click to toggle source

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
use_cas(options) click to toggle source
# File lib/diplomat/rest_client.rb, line 214
def use_cas(options)
  options ? use_named_parameter('cas', options[:cas]) : []
end
use_consistency(options) click to toggle source
# File lib/diplomat/rest_client.rb, line 218
def use_consistency(options)
  options[:consistency] ? [options[:consistency].to_s] : []
end
valid_transaction_verbs() click to toggle source

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
valid_value_transactions() click to toggle source

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