module Kubeclient::ClientMixin

Common methods this is mixed in by other gems

Constants

DEFAULT_AUTH_OPTIONS
DEFAULT_HTTP_MAX_REDIRECTS
DEFAULT_HTTP_PROXY_URI
DEFAULT_SOCKET_OPTIONS
DEFAULT_SSL_OPTIONS
DEFAULT_TIMEOUTS
ENTITY_METHODS
IRREGULAR_NAMES
SEARCH_ARGUMENTS
WATCH_ARGUMENTS

Attributes

api_endpoint[R]
auth_options[R]
discovered[R]
headers[R]
http_max_redirects[R]
http_proxy_uri[R]
ssl_options[R]

Public Class Methods

parse_definition(kind, name) click to toggle source
# File lib/kubeclient/common.rb, line 138
def self.parse_definition(kind, name)
  # Kubernetes gives us 3 inputs:
  #   kind: "ComponentStatus", "NetworkPolicy", "Endpoints"
  #   name: "componentstatuses", "networkpolicies", "endpoints"
  #   singularName: "componentstatus" etc (usually omitted, defaults to kind.downcase)
  # and want to derive singular and plural method names, with underscores:
  #   "network_policy"
  #   "network_policies"
  # kind's CamelCase word boundaries determine our placement of underscores.

  if IRREGULAR_NAMES[kind]
    # In a few cases, the given kind / singularName itself is still plural.
    # We require a distinct singular method name, so force it.
    method_names = IRREGULAR_NAMES[kind]
  else
    # TODO: respect singularName from discovery?
    # But how?  If it differs from kind.downcase, kind's word boundaries don't apply.
    singular_name = kind.downcase

    if !(/[A-Z]/ =~ kind)
      # Some custom resources have a fully lowercase kind - can't infer underscores.
      method_names = [singular_name, name]
    else
      # Some plurals are not exact suffixes, e.g. NetworkPolicy -> networkpolicies.
      # So don't expect full last word to match.
      /^(?<prefix>(.*[A-Z]))(?<singular_suffix>[^A-Z]*)$/ =~ kind  # "NetworkP", "olicy"
      if name.start_with?(prefix.downcase)
        plural_suffix = name[prefix.length..-1]                    # "olicies"
        prefix_underscores = ClientMixin.underscore_entity(prefix) # "network_p"
        method_names = [prefix_underscores + singular_suffix,      # "network_policy"
                        prefix_underscores + plural_suffix]        # "network_policies"
      else
        method_names = resolve_unconventional_method_names(name, kind, singular_name)
      end
    end
  end

  OpenStruct.new(
    entity_type:   kind,
    resource_name: name,
    method_names:  method_names
  )
end
resolve_unconventional_method_names(name, kind, singular_name) click to toggle source
# File lib/kubeclient/common.rb, line 182
def self.resolve_unconventional_method_names(name, kind, singular_name)
  underscored_name = name.tr('-', '_')
  singular_underscores = ClientMixin.underscore_entity(kind)
  if underscored_name.start_with?(singular_underscores)
    [singular_underscores, underscored_name]
  else
    # fallback to lowercase, no separators for both names
    [singular_name, underscored_name.tr('_', '')]
  end
end
underscore_entity(entity_name) click to toggle source

rubocop:enable Metrics/BlockLength

# File lib/kubeclient/common.rb, line 260
def self.underscore_entity(entity_name)
  entity_name.gsub(/([a-z])([A-Z])/, '\1_\2').downcase
end

Public Instance Methods

all_entities(options = {}) click to toggle source
# File lib/kubeclient/common.rb, line 409
def all_entities(options = {})
  discover unless @discovered
  @entities.values.each_with_object({}) do |entity, result_hash|
    # method call for get each entities
    # build hash of entity name to array of the entities
    method_name = "get_#{entity.method_names[1]}"
    begin
      result_hash[entity.method_names[0]] = send(method_name, options)
    rescue Kubeclient::HttpError
      next # do not fail due to resources not supporting get
    end
  end
end
api() click to toggle source
# File lib/kubeclient/common.rb, line 483
def api
  response = handle_exception { create_rest_client.get(@headers) }
  JSON.parse(response)
end
api_valid?() click to toggle source
# File lib/kubeclient/common.rb, line 476
def api_valid?
  result = api
  result.is_a?(Hash) && (result['versions'] || []).any? do |group|
    @api_group.empty? ? group.include?(@api_version) : group['version'] == @api_version
  end
end
build_namespace_prefix(namespace) click to toggle source
# File lib/kubeclient/common.rb, line 202
def build_namespace_prefix(namespace)
  namespace.to_s.empty? ? '' : "namespaces/#{namespace}/"
end
create_entity(entity_type, resource_name, entity_config) click to toggle source
# File lib/kubeclient/common.rb, line 368
def create_entity(entity_type, resource_name, entity_config)
  # Duplicate the entity_config to a hash so that when we assign
  # kind and apiVersion, this does not mutate original entity_config obj.
  hash = entity_config.to_hash

  ns_prefix = build_namespace_prefix(hash[:metadata][:namespace])

  # TODO: temporary solution to add "kind" and apiVersion to request
  # until this issue is solved
  # https://github.com/GoogleCloudPlatform/kubernetes/issues/6439
  hash[:kind] = entity_type
  hash[:apiVersion] = @api_group + @api_version
  response = handle_exception do
    rest_client[ns_prefix + resource_name]
      .post(hash.to_json, { 'Content-Type' => 'application/json' }.merge(@headers))
  end
  format_response(@as, response.body)
end
create_rest_client(path = nil) click to toggle source
# File lib/kubeclient/common.rb, line 264
def create_rest_client(path = nil)
  path ||= @api_endpoint.path
  options = {
    ssl_ca_file: @ssl_options[:ca_file],
    ssl_cert_store: @ssl_options[:cert_store],
    verify_ssl: @ssl_options[:verify_ssl],
    ssl_client_cert: @ssl_options[:client_cert],
    ssl_client_key: @ssl_options[:client_key],
    proxy: @http_proxy_uri,
    max_redirects: @http_max_redirects,
    user: @auth_options[:username],
    password: @auth_options[:password],
    open_timeout: @timeouts[:open],
    read_timeout: @timeouts[:read]
  }
  RestClient::Resource.new(@api_endpoint.merge(path).to_s, options)
end
define_entity_methods() click to toggle source

rubocop:disable Metrics/BlockLength

# File lib/kubeclient/common.rb, line 207
def define_entity_methods
  @entities.values.each do |entity|
    # get all entities of a type e.g. get_nodes, get_pods, etc.
    define_singleton_method("get_#{entity.method_names[1]}") do |options = {}|
      get_entities(entity.entity_type, entity.resource_name, options)
    end

    # watch all entities of a type e.g. watch_nodes, watch_pods, etc.
    define_singleton_method("watch_#{entity.method_names[1]}") do |options = {}|
      # This method used to take resource_version as a param, so
      # this conversion is to keep backwards compatibility
      options = { resource_version: options } unless options.is_a?(Hash)

      watch_entities(entity.resource_name, options)
    end

    # get a single entity of a specific type by name
    define_singleton_method("get_#{entity.method_names[0]}") \
    do |name, namespace = nil, opts = {}|
      get_entity(entity.resource_name, name, namespace, opts)
    end

    define_singleton_method("delete_#{entity.method_names[0]}") \
    do |name, namespace = nil, opts = {}|
      delete_entity(entity.resource_name, name, namespace, opts)
    end

    define_singleton_method("create_#{entity.method_names[0]}") do |entity_config|
      create_entity(entity.entity_type, entity.resource_name, entity_config)
    end

    define_singleton_method("update_#{entity.method_names[0]}") do |entity_config|
      update_entity(entity.resource_name, entity_config)
    end

    define_singleton_method("patch_#{entity.method_names[0]}") \
    do |name, patch, namespace = nil|
      patch_entity(entity.resource_name, name, patch, 'strategic-merge-patch', namespace)
    end

    define_singleton_method("json_patch_#{entity.method_names[0]}") \
    do |name, patch, namespace = nil|
      patch_entity(entity.resource_name, name, patch, 'json-patch', namespace)
    end

    define_singleton_method("merge_patch_#{entity.method_names[0]}") \
    do |name, patch, namespace = nil|
      patch_entity(entity.resource_name, name, patch, 'merge-patch', namespace)
    end
  end
end
delete_entity(resource_name, name, namespace = nil, delete_options: {}) click to toggle source

delete_options are passed as a JSON payload in the delete request

# File lib/kubeclient/common.rb, line 350
def delete_entity(resource_name, name, namespace = nil, delete_options: {})
  delete_options_hash = delete_options.to_hash
  ns_prefix = build_namespace_prefix(namespace)
  payload = delete_options_hash.to_json unless delete_options_hash.empty?
  response = handle_exception do
    rs = rest_client[ns_prefix + resource_name + "/#{name}"]
    RestClient::Request.execute(
      rs.options.merge(
        method: :delete,
        url: rs.url,
        headers: { 'Content-Type' => 'application/json' }.merge(@headers),
        payload: payload
      )
    )
  end
  format_response(@as, response.body)
end
discover() click to toggle source
# File lib/kubeclient/common.rb, line 132
def discover
  load_entities
  define_entity_methods
  @discovered = true
end
discovery_needed?(method_sym) click to toggle source
# File lib/kubeclient/common.rb, line 115
def discovery_needed?(method_sym)
  !@discovered && ENTITY_METHODS.any? { |x| method_sym.to_s.start_with?(x) }
end
get_entities(entity_type, resource_name, options = {}) click to toggle source

Accepts the following options:

:namespace (string) - the namespace of the entity.
:label_selector (string) - a selector to restrict the list of returned objects by labels.
:field_selector (string) - a selector to restrict the list of returned objects by fields.
:limit (integer) - a maximum number of items to return in each response
:continue (string) - a token used to retrieve the next chunk of entities
:as (:raw|:ros) - defaults to :ros
  :raw - return the raw response body as a string
  :ros - return a collection of RecursiveOpenStruct objects
# File lib/kubeclient/common.rb, line 324
def get_entities(entity_type, resource_name, options = {})
  params = {}
  SEARCH_ARGUMENTS.each { |k, v| params[k] = options[v] if options[v] }

  ns_prefix = build_namespace_prefix(options[:namespace])
  response = handle_exception do
    rest_client[ns_prefix + resource_name]
      .get({ 'params' => params }.merge(@headers))
  end
  format_response(options[:as] || @as, response.body, entity_type)
end
get_entity(resource_name, name, namespace = nil, options = {}) click to toggle source

Accepts the following options:

:as (:raw|:ros) - defaults to :ros
  :raw - return the raw response body as a string
  :ros - return a collection of RecursiveOpenStruct objects
# File lib/kubeclient/common.rb, line 340
def get_entity(resource_name, name, namespace = nil, options = {})
  ns_prefix = build_namespace_prefix(namespace)
  response = handle_exception do
    rest_client[ns_prefix + resource_name + "/#{name}"]
      .get(@headers)
  end
  format_response(options[:as] || @as, response.body)
end
get_pod_log(pod_name, namespace, container: nil, previous: false, timestamps: false, since_time: nil, tail_lines: nil) click to toggle source
# File lib/kubeclient/common.rb, line 423
def get_pod_log(pod_name, namespace,
                container: nil, previous: false,
                timestamps: false, since_time: nil, tail_lines: nil)
  params = {}
  params[:previous] = true if previous
  params[:container] = container if container
  params[:timestamps] = timestamps if timestamps
  params[:sinceTime] = format_datetime(since_time) if since_time
  params[:tailLines] = tail_lines if tail_lines

  ns = build_namespace_prefix(namespace)
  handle_exception do
    rest_client[ns + "pods/#{pod_name}/log"]
      .get({ 'params' => params }.merge(@headers))
  end
end
handle_exception() { || ... } click to toggle source
# File lib/kubeclient/common.rb, line 119
def handle_exception
  yield
rescue RestClient::Exception => e
  json_error_msg = begin
    JSON.parse(e.response || '') || {}
  rescue JSON::ParserError
    {}
  end
  err_message = json_error_msg['message'] || e.message
  error_klass = e.http_code == 404 ? ResourceNotFoundError : HttpError
  raise error_klass.new(e.http_code, err_message, e.response)
end
handle_uri(uri, path) click to toggle source
# File lib/kubeclient/common.rb, line 193
def handle_uri(uri, path)
  raise ArgumentError, 'Missing uri' unless uri
  @api_endpoint = (uri.is_a?(URI) ? uri : URI.parse(uri))
  @api_endpoint.path = path if @api_endpoint.path.empty?
  @api_endpoint.path = @api_endpoint.path.chop if @api_endpoint.path.end_with?('/')
  components = @api_endpoint.path.to_s.split('/') # ["", "api"] or ["", "apis", batch]
  @api_group = components.length > 2 ? components[2] + '/' : ''
end
initialize_client( uri, path, version, ssl_options: DEFAULT_SSL_OPTIONS, auth_options: DEFAULT_AUTH_OPTIONS, socket_options: DEFAULT_SOCKET_OPTIONS, timeouts: DEFAULT_TIMEOUTS, http_proxy_uri: DEFAULT_HTTP_PROXY_URI, http_max_redirects: DEFAULT_HTTP_MAX_REDIRECTS, as: :ros ) click to toggle source
# File lib/kubeclient/common.rb, line 60
def initialize_client(
  uri,
  path,
  version,
  ssl_options: DEFAULT_SSL_OPTIONS,
  auth_options: DEFAULT_AUTH_OPTIONS,
  socket_options: DEFAULT_SOCKET_OPTIONS,
  timeouts: DEFAULT_TIMEOUTS,
  http_proxy_uri: DEFAULT_HTTP_PROXY_URI,
  http_max_redirects: DEFAULT_HTTP_MAX_REDIRECTS,
  as: :ros
)
  validate_auth_options(auth_options)
  handle_uri(uri, path)

  @entities = {}
  @discovered = false
  @api_version = version
  @headers = {}
  @ssl_options = ssl_options
  @auth_options = auth_options
  @socket_options = socket_options
  # Allow passing partial timeouts hash, without unspecified
  # @timeouts[:foo] == nil resulting in infinite timeout.
  @timeouts = DEFAULT_TIMEOUTS.merge(timeouts)
  @http_proxy_uri = http_proxy_uri ? http_proxy_uri.to_s : nil
  @http_max_redirects = http_max_redirects
  @as = as

  if auth_options[:bearer_token]
    bearer_token(@auth_options[:bearer_token])
  elsif auth_options[:bearer_token_file]
    validate_bearer_token_file
    bearer_token(File.read(@auth_options[:bearer_token_file]))
  end
end
method_missing(method_sym, *args, &block) click to toggle source
Calls superclass method
# File lib/kubeclient/common.rb, line 97
def method_missing(method_sym, *args, &block)
  if discovery_needed?(method_sym)
    discover
    send(method_sym, *args, &block)
  else
    super
  end
end
patch_entity(resource_name, name, patch, strategy, namespace) click to toggle source
# File lib/kubeclient/common.rb, line 397
def patch_entity(resource_name, name, patch, strategy, namespace)
  ns_prefix = build_namespace_prefix(namespace)
  response = handle_exception do
    rest_client[ns_prefix + resource_name + "/#{name}"]
      .patch(
        patch.to_json,
        { 'Content-Type' => "application/#{strategy}+json" }.merge(@headers)
      )
  end
  format_response(@as, response.body)
end
process_template(template) click to toggle source
# File lib/kubeclient/common.rb, line 467
def process_template(template)
  ns_prefix = build_namespace_prefix(template[:metadata][:namespace])
  response = handle_exception do
    rest_client[ns_prefix + 'processedtemplates']
      .post(template.to_h.to_json, { 'Content-Type' => 'application/json' }.merge(@headers))
  end
  JSON.parse(response)
end
proxy_url(kind, name, port, namespace = '') click to toggle source
# File lib/kubeclient/common.rb, line 455
def proxy_url(kind, name, port, namespace = '')
  discover unless @discovered
  entity_name_plural =
    if %w[services pods nodes].include?(kind.to_s)
      kind.to_s
    else
      @entities[kind.to_s].resource_name
    end
  ns_prefix = build_namespace_prefix(namespace)
  rest_client["#{ns_prefix}#{entity_name_plural}/#{name}:#{port}/proxy"].url
end
respond_to_missing?(method_sym, include_private = false) click to toggle source
Calls superclass method
# File lib/kubeclient/common.rb, line 106
def respond_to_missing?(method_sym, include_private = false)
  if discovery_needed?(method_sym)
    discover
    respond_to?(method_sym, include_private)
  else
    super
  end
end
rest_client() click to toggle source
# File lib/kubeclient/common.rb, line 282
def rest_client
  @rest_client ||= begin
    create_rest_client("#{@api_endpoint.path}/#{@api_version}")
  end
end
update_entity(resource_name, entity_config) click to toggle source
# File lib/kubeclient/common.rb, line 387
def update_entity(resource_name, entity_config)
  name      = entity_config[:metadata][:name]
  ns_prefix = build_namespace_prefix(entity_config[:metadata][:namespace])
  response = handle_exception do
    rest_client[ns_prefix + resource_name + "/#{name}"]
      .put(entity_config.to_h.to_json, { 'Content-Type' => 'application/json' }.merge(@headers))
  end
  format_response(@as, response.body)
end
watch_entities(resource_name, options = {}) click to toggle source

Accepts the following options:

:namespace (string) - the namespace of the entity.
:name (string) - the name of the entity to watch.
:label_selector (string) - a selector to restrict the list of returned objects by labels.
:field_selector (string) - a selector to restrict the list of returned objects by fields.
:resource_version (string) - shows changes that occur after passed version of a resource.
:as (:raw|:ros) - defaults to :ros
  :raw - return the raw response body as a string
  :ros - return a collection of RecursiveOpenStruct objects
# File lib/kubeclient/common.rb, line 297
def watch_entities(resource_name, options = {})
  ns = build_namespace_prefix(options[:namespace])

  path = "watch/#{ns}#{resource_name}"
  path += "/#{options[:name]}" if options[:name]
  uri = @api_endpoint.merge("#{@api_endpoint.path}/#{@api_version}/#{path}")

  params = {}
  WATCH_ARGUMENTS.each { |k, v| params[k] = options[v] if options[v] }
  uri.query = URI.encode_www_form(params) if params.any?

  Kubeclient::Common::WatchStream.new(
    uri,
    http_options(uri),
    formatter: ->(value) { format_response(options[:as] || @as, value) }
  )
end
watch_pod_log(pod_name, namespace, container: nil) click to toggle source
# File lib/kubeclient/common.rb, line 440
def watch_pod_log(pod_name, namespace, container: nil)
  # Adding the "follow=true" query param tells the Kubernetes API to keep
  # the connection open and stream updates to the log.
  params = { follow: true }
  params[:container] = container if container

  ns = build_namespace_prefix(namespace)

  uri = @api_endpoint.dup
  uri.path += "/#{@api_version}/#{ns}pods/#{pod_name}/log"
  uri.query = URI.encode_www_form(params)

  Kubeclient::Common::WatchStream.new(uri, http_options(uri), formatter: ->(value) { value })
end

Private Instance Methods

bearer_token(bearer_token) click to toggle source
# File lib/kubeclient/common.rb, line 561
def bearer_token(bearer_token)
  @headers ||= {}
  @headers[:Authorization] = "Bearer #{bearer_token}"
end
fetch_entities() click to toggle source
# File lib/kubeclient/common.rb, line 557
def fetch_entities
  JSON.parse(handle_exception { rest_client.get(@headers) })
end
format_datetime(value) click to toggle source

Format datetime according to RFC3339

# File lib/kubeclient/common.rb, line 499
def format_datetime(value)
  case value
  when DateTime, Time
    value.strftime('%FT%T.%9N%:z')
  when String
    value
  else
    raise ArgumentError, "unsupported type '#{value.class}' of time value '#{value}'"
  end
end
format_response(as, body, list_type = nil) click to toggle source
# File lib/kubeclient/common.rb, line 510
def format_response(as, body, list_type = nil)
  case as
  when :raw
    body
  when :parsed
    JSON.parse(body)
  when :parsed_symbolized
    JSON.parse(body, symbolize_names: true)
  when :ros
    result = JSON.parse(body)

    if list_type
      resource_version =
        result.fetch('resourceVersion') do
          result.fetch('metadata', {}).fetch('resourceVersion', nil)
        end

      # If 'limit' was passed save the continue token
      # see https://kubernetes.io/docs/reference/using-api/api-concepts/#retrieving-large-results-sets-in-chunks
      continue = result.fetch('metadata', {}).fetch('continue', nil)

      # result['items'] might be nil due to https://github.com/kubernetes/kubernetes/issues/13096
      collection = result['items'].to_a.map { |item| Kubeclient::Resource.new(item) }

      Kubeclient::Common::EntityList.new(list_type, resource_version, collection, continue)
    else
      Kubeclient::Resource.new(result)
    end
  else
    raise ArgumentError, "Unsupported format #{as.inspect}"
  end
end
http_options(uri) click to toggle source
# File lib/kubeclient/common.rb, line 589
def http_options(uri)
  options = {
    basic_auth_user: @auth_options[:username],
    basic_auth_password: @auth_options[:password],
    headers: @headers,
    http_proxy_uri: @http_proxy_uri,
    http_max_redirects: http_max_redirects
  }

  if uri.scheme == 'https'
    options[:ssl] = {
      ca_file: @ssl_options[:ca_file],
      cert: @ssl_options[:client_cert],
      cert_store: @ssl_options[:cert_store],
      key: @ssl_options[:client_key],
      # ruby HTTP uses verify_mode instead of verify_ssl
      # http://ruby-doc.org/stdlib-1.9.3/libdoc/openssl/rdoc/OpenSSL/SSL/SSLContext.html
      verify_mode: @ssl_options[:verify_ssl]
    }
  end

  options.merge(@socket_options)
end
load_entities() click to toggle source
# File lib/kubeclient/common.rb, line 543
def load_entities
  @entities = {}
  fetch_entities['resources'].each do |resource|
    next if resource['name'].include?('/')
    # Not a regular entity, special functionality covered by `process_template`.
    # https://github.com/openshift/origin/issues/21668
    next if resource['kind'] == 'Template' && resource['name'] == 'processedtemplates'
    resource['kind'] ||=
      Kubeclient::Common::MissingKindCompatibility.resource_kind(resource['name'])
    entity = ClientMixin.parse_definition(resource['kind'], resource['name'])
    @entities[entity.method_names[0]] = entity if entity
  end
end
validate_auth_options(opts) click to toggle source
# File lib/kubeclient/common.rb, line 566
def validate_auth_options(opts)
  # maintain backward compatibility:
  opts[:username] = opts[:user] if opts[:user]

  if %i[bearer_token bearer_token_file username].count { |key| opts[key] } > 1
    raise(
      ArgumentError,
      'Invalid auth options: specify only one of username/password,' \
      ' bearer_token or bearer_token_file'
    )
  elsif %i[username password].count { |key| opts[key] } == 1
    raise ArgumentError, 'Basic auth requires both username & password'
  end
end
validate_bearer_token_file() click to toggle source
# File lib/kubeclient/common.rb, line 581
def validate_bearer_token_file
  msg = "Token file #{@auth_options[:bearer_token_file]} does not exist"
  raise ArgumentError, msg unless File.file?(@auth_options[:bearer_token_file])

  msg = "Cannot read token file #{@auth_options[:bearer_token_file]}"
  raise ArgumentError, msg unless File.readable?(@auth_options[:bearer_token_file])
end