Module: Kubeclient::ClientMixin

Included in:
Client
Defined in:
lib/kubeclient/common.rb

Overview

Common methods this is mixed in by other gems

Constant Summary collapse

ENTITY_METHODS =
%w(get watch delete create update patch)
DEFAULT_SSL_OPTIONS =
{
  client_cert: nil,
  client_key:  nil,
  ca_file:     nil,
  cert_store:  nil,
  verify_ssl:  OpenSSL::SSL::VERIFY_PEER
}.freeze
DEFAULT_AUTH_OPTIONS =
{
  username:          nil,
  password:          nil,
  bearer_token:      nil,
  bearer_token_file: nil
}.freeze
DEFAULT_SOCKET_OPTIONS =
{
  socket_class:     nil,
  ssl_socket_class: nil
}.freeze
DEFAULT_TIMEOUTS =
{
  # These do NOT affect watch, watching never times out.
  open: Net::HTTP.new('127.0.0.1').open_timeout, # depends on ruby version
  read: Net::HTTP.new('127.0.0.1').read_timeout
}.freeze
DEFAULT_HTTP_PROXY_URI =
nil
SEARCH_ARGUMENTS =
{
  'labelSelector' => :label_selector,
  'fieldSelector' => :field_selector
}.freeze
WATCH_ARGUMENTS =
{ 'resourceVersion' => :resource_version }.merge!(SEARCH_ARGUMENTS).freeze

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Dynamic Method Handling

This class handles dynamic methods through the method_missing method

#method_missing(method_sym, *args, &block) ⇒ Object



86
87
88
89
90
91
92
93
# File 'lib/kubeclient/common.rb', line 86

def method_missing(method_sym, *args, &block)
  if discovery_needed?(method_sym)
    discover
    send(method_sym, *args, &block)
  else
    super
  end
end

Instance Attribute Details

#api_endpointObject (readonly)

Returns the value of attribute api_endpoint.



44
45
46
# File 'lib/kubeclient/common.rb', line 44

def api_endpoint
  @api_endpoint
end

#auth_optionsObject (readonly)

Returns the value of attribute auth_options.



46
47
48
# File 'lib/kubeclient/common.rb', line 46

def auth_options
  @auth_options
end

#discoveredObject (readonly)

Returns the value of attribute discovered.



49
50
51
# File 'lib/kubeclient/common.rb', line 49

def discovered
  @discovered
end

#headersObject (readonly)

Returns the value of attribute headers.



48
49
50
# File 'lib/kubeclient/common.rb', line 48

def headers
  @headers
end

#http_proxy_uriObject (readonly)

Returns the value of attribute http_proxy_uri.



47
48
49
# File 'lib/kubeclient/common.rb', line 47

def http_proxy_uri
  @http_proxy_uri
end

#ssl_optionsObject (readonly)

Returns the value of attribute ssl_options.



45
46
47
# File 'lib/kubeclient/common.rb', line 45

def ssl_options
  @ssl_options
end

Class Method Details

.parse_definition(kind, name) ⇒ Object



126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
# File 'lib/kubeclient/common.rb', line 126

def self.parse_definition(kind, name)
  # "name": "componentstatuses", networkpolicies, endpoints
  # "kind": "ComponentStatus" NetworkPolicy, Endpoints
  # maintain pre group api compatibility for endpoints.
  # See: https://github.com/kubernetes/kubernetes/issues/8115
  kind = 'Endpoint' if kind == 'Endpoints'

  prefix = kind[0..kind.rindex(/[A-Z]/)] # NetworkP
  m = name.match(/^#{prefix.downcase}(.*)$/)
  m && OpenStruct.new(
    entity_type:   kind, # ComponentStatus
    resource_name: name, # componentstatuses
    method_names:  [
      ClientMixin.underscore_entity(kind),         # component_status
      ClientMixin.underscore_entity(prefix) + m[1] # component_statuses
    ]
  )
end

.resource_class(class_owner, entity_type) ⇒ Object



158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
# File 'lib/kubeclient/common.rb', line 158

def self.resource_class(class_owner, entity_type)
  if class_owner.const_defined?(entity_type, false)
    class_owner.const_get(entity_type, false)
  else
    class_owner.const_set(
      entity_type,
      Class.new(RecursiveOpenStruct) do
        def initialize(hash = nil, args = {})
          args[:recurse_over_arrays] = true
          super(hash, args)
        end
      end
    )
  end
end

.restclient_read_timeout_optionObject



435
436
437
438
439
440
441
442
443
444
445
# File 'lib/kubeclient/common.rb', line 435

def self.restclient_read_timeout_option
  @restclient_read_timeout_option ||=
    # RestClient silently accepts unknown options, so check accessors instead.
    if RestClient::Resource.instance_methods.include?(:read_timeout) # rest-client 2.0
      :read_timeout
    elsif RestClient::Resource.instance_methods.include?(:timeout) # rest-client 1.x
      :timeout
    else
      fail ArgumentError("RestClient doesn't support neither :read_timeout nor :timeout")
    end
end

.underscore_entity(entity_name) ⇒ Object



214
215
216
# File 'lib/kubeclient/common.rb', line 214

def self.underscore_entity(entity_name)
  entity_name.gsub(/([a-z])([A-Z])/, '\1_\2').downcase
end

Instance Method Details

#all_entitiesObject



357
358
359
360
361
362
363
364
365
366
367
368
369
# File 'lib/kubeclient/common.rb', line 357

def all_entities
  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)
    rescue KubeException
      next # do not fail due to resources not supporting get
    end
  end
end

#apiObject



430
431
432
433
# File 'lib/kubeclient/common.rb', line 430

def api
  response = handle_exception { create_rest_client.get(@headers) }
  JSON.parse(response)
end

#api_valid?Boolean

Returns:

  • (Boolean)


423
424
425
426
427
428
# File 'lib/kubeclient/common.rb', line 423

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) ⇒ Object



154
155
156
# File 'lib/kubeclient/common.rb', line 154

def build_namespace_prefix(namespace)
  namespace.to_s.empty? ? '' : "namespaces/#{namespace}/"
end

#create_entity(entity_type, resource_name, entity_config, klass) ⇒ Object



307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
# File 'lib/kubeclient/common.rb', line 307

def create_entity(entity_type, resource_name, entity_config, klass)
  # 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
  # TODO: #2 solution for
  # https://github.com/kubernetes/kubernetes/issues/8115
  if entity_type.eql? 'Endpoint'
    hash[:kind] = 'Endpoints'
  else
    hash[:kind] = entity_type
  end
  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
  result = JSON.parse(response)
  new_entity(result, klass)
end

#create_rest_client(path = nil) ⇒ Object



218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
# File 'lib/kubeclient/common.rb', line 218

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,
    user: @auth_options[:username],
    password: @auth_options[:password],
    open_timeout: @timeouts[:open],
    ClientMixin.restclient_read_timeout_option => @timeouts[:read]
  }
  RestClient::Resource.new(@api_endpoint.merge(path).to_s, options)
end

#define_entity_methodsObject



174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
# File 'lib/kubeclient/common.rb', line 174

def define_entity_methods
  @entities.values.each do |entity|
    klass = ClientMixin.resource_class(@class_owner, entity.entity_type)
    # 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, klass, 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|
      get_entity(klass, entity.resource_name, name, namespace)
    end

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

    define_singleton_method("create_#{entity.method_names[0]}") do |entity_config|
      create_entity(entity.entity_type, entity.resource_name, entity_config, klass)
    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, namespace)
    end
  end
end

#delete_entity(resource_name, name, namespace = nil) ⇒ Object



299
300
301
302
303
304
305
# File 'lib/kubeclient/common.rb', line 299

def delete_entity(resource_name, name, namespace = nil)
  ns_prefix = build_namespace_prefix(namespace)
  handle_exception do
    rest_client[ns_prefix + resource_name + "/#{name}"]
      .delete(@headers)
  end
end

#discoverObject



120
121
122
123
124
# File 'lib/kubeclient/common.rb', line 120

def discover
  load_entities
  define_entity_methods
  @discovered = true
end

#discovery_needed?(method_sym) ⇒ Boolean

Returns:

  • (Boolean)


104
105
106
# File 'lib/kubeclient/common.rb', line 104

def discovery_needed?(method_sym)
  !@discovered && ENTITY_METHODS.any? { |x| method_sym.to_s.start_with?(x) }
end

#get_entities(entity_type, klass, resource_name, options = {}) ⇒ Object

Accepts the following string options:

:namespace - the namespace of the entity.
:label_selector - a selector to restrict the list of returned objects by their labels.
:field_selector - a selector to restrict the list of returned objects by their fields.


265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
# File 'lib/kubeclient/common.rb', line 265

def get_entities(entity_type, klass, 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

  result = JSON.parse(response)

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

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

  Kubeclient::Common::EntityList.new(entity_type, resource_version, collection)
end

#get_entity(klass, resource_name, name, namespace = nil) ⇒ Object



289
290
291
292
293
294
295
296
297
# File 'lib/kubeclient/common.rb', line 289

def get_entity(klass, resource_name, name, namespace = nil)
  ns_prefix = build_namespace_prefix(namespace)
  response = handle_exception do
    rest_client[ns_prefix + resource_name + "/#{name}"]
    .get(@headers)
  end
  result = JSON.parse(response)
  new_entity(result, klass)
end

#get_pod_log(pod_name, namespace, container: nil, previous: false) ⇒ Object



371
372
373
374
375
376
377
378
379
380
381
# File 'lib/kubeclient/common.rb', line 371

def get_pod_log(pod_name, namespace, container: nil, previous: false)
  params = {}
  params[:previous] = true if previous
  params[:container] = container if container

  ns = build_namespace_prefix(namespace)
  handle_exception do
    rest_client[ns + "pods/#{pod_name}/log"]
      .get({ 'params' => params }.merge(@headers))
  end
end

#handle_exceptionObject



108
109
110
111
112
113
114
115
116
117
118
# File 'lib/kubeclient/common.rb', line 108

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
  raise KubeException.new(e.http_code, err_message, e.response)
end

#handle_uri(uri, path) ⇒ Object



145
146
147
148
149
150
151
152
# File 'lib/kubeclient/common.rb', line 145

def handle_uri(uri, path)
  fail 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(class_owner, 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) ⇒ Object



51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
# File 'lib/kubeclient/common.rb', line 51

def initialize_client(
  class_owner,
  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
)
  validate_auth_options(auth_options)
  handle_uri(uri, path)

  @class_owner = class_owner
  @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.to_s if http_proxy_uri

  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

#new_entity(hash, klass) ⇒ Object



353
354
355
# File 'lib/kubeclient/common.rb', line 353

def new_entity(hash, klass)
  klass.new(hash)
end

#patch_entity(resource_name, name, patch, namespace = nil) ⇒ Object



342
343
344
345
346
347
348
349
350
351
# File 'lib/kubeclient/common.rb', line 342

def patch_entity(resource_name, name, patch, namespace = nil)
  ns_prefix = build_namespace_prefix(namespace)
  handle_exception do
    rest_client[ns_prefix + resource_name + "/#{name}"]
      .patch(
        patch.to_json,
        { 'Content-Type' => 'application/strategic-merge-patch+json' }.merge(@headers)
      )
  end
end

#process_template(template) ⇒ Object



414
415
416
417
418
419
420
421
# File 'lib/kubeclient/common.rb', line 414

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 = '') ⇒ Object



398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
# File 'lib/kubeclient/common.rb', line 398

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)
  # TODO: Change this once services supports the new scheme
  if entity_name_plural == 'pods'
    rest_client["#{ns_prefix}#{entity_name_plural}/#{name}:#{port}/proxy"].url
  else
    rest_client["proxy/#{ns_prefix}#{entity_name_plural}/#{name}:#{port}"].url
  end
end

#respond_to_missing?(method_sym, include_private = false) ⇒ Boolean

Returns:

  • (Boolean)


95
96
97
98
99
100
101
102
# File 'lib/kubeclient/common.rb', line 95

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_clientObject



235
236
237
238
239
# File 'lib/kubeclient/common.rb', line 235

def rest_client
  @rest_client ||= begin
    create_rest_client("#{@api_endpoint.path}/#{@api_version}")
  end
end

#update_entity(resource_name, entity_config) ⇒ Object



333
334
335
336
337
338
339
340
# File 'lib/kubeclient/common.rb', line 333

def update_entity(resource_name, entity_config)
  name      = entity_config[:metadata][:name]
  ns_prefix = build_namespace_prefix(entity_config[:metadata][:namespace])
  handle_exception do
    rest_client[ns_prefix + resource_name + "/#{name}"]
      .put(entity_config.to_h.to_json, { 'Content-Type' => 'application/json' }.merge(@headers))
  end
end

#watch_entities(resource_name, options = {}) ⇒ Object

Accepts the following string options:

:namespace - the namespace of the entity.
:name - the name of the entity to watch.
:label_selector - a selector to restrict the list of returned objects by their labels.
:field_selector - a selector to restrict the list of returned objects by their fields.
:resource_version - shows changes that occur after that particular version of a resource.


247
248
249
250
251
252
253
254
255
256
257
258
259
# File 'lib/kubeclient/common.rb', line 247

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))
end

#watch_pod_log(pod_name, namespace, container: nil) ⇒ Object



383
384
385
386
387
388
389
390
391
392
393
394
395
396
# File 'lib/kubeclient/common.rb', line 383

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), format: :text)
end