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