kubeclient 0.3.0 → 4.9.2

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of kubeclient might be problematic. Click here for more details.

Files changed (63) hide show
  1. checksums.yaml +5 -5
  2. data/.github/workflows/actions.yml +35 -0
  3. data/.gitignore +1 -0
  4. data/.rubocop.yml +29 -0
  5. data/CHANGELOG.md +208 -0
  6. data/Gemfile +3 -0
  7. data/README.md +706 -57
  8. data/RELEASING.md +69 -0
  9. data/Rakefile +3 -5
  10. data/kubeclient.gemspec +19 -11
  11. data/lib/kubeclient/aws_eks_credentials.rb +46 -0
  12. data/lib/kubeclient/common.rb +597 -161
  13. data/lib/kubeclient/config.rb +195 -0
  14. data/lib/kubeclient/entity_list.rb +7 -2
  15. data/lib/kubeclient/exec_credentials.rb +89 -0
  16. data/lib/kubeclient/gcp_auth_provider.rb +19 -0
  17. data/lib/kubeclient/gcp_command_credentials.rb +31 -0
  18. data/lib/kubeclient/google_application_default_credentials.rb +31 -0
  19. data/lib/kubeclient/http_error.rb +25 -0
  20. data/lib/kubeclient/missing_kind_compatibility.rb +68 -0
  21. data/lib/kubeclient/oidc_auth_provider.rb +52 -0
  22. data/lib/kubeclient/resource.rb +11 -0
  23. data/lib/kubeclient/resource_not_found_error.rb +4 -0
  24. data/lib/kubeclient/version.rb +1 -1
  25. data/lib/kubeclient/watch_stream.rb +71 -28
  26. data/lib/kubeclient.rb +25 -82
  27. metadata +140 -114
  28. data/.travis.yml +0 -6
  29. data/lib/kubeclient/kube_exception.rb +0 -13
  30. data/lib/kubeclient/watch_notice.rb +0 -7
  31. data/test/json/created_namespace_b3.json +0 -20
  32. data/test/json/created_secret.json +0 -16
  33. data/test/json/created_service_b3.json +0 -31
  34. data/test/json/empty_pod_list_b3.json +0 -9
  35. data/test/json/endpoint_list_b3.json +0 -48
  36. data/test/json/entity_list_b3.json +0 -56
  37. data/test/json/event_list_b3.json +0 -35
  38. data/test/json/namespace_b3.json +0 -13
  39. data/test/json/namespace_exception_b3.json +0 -8
  40. data/test/json/namespace_list_b3.json +0 -32
  41. data/test/json/node_b3.json +0 -29
  42. data/test/json/node_list_b3.json +0 -37
  43. data/test/json/pod_b3.json +0 -92
  44. data/test/json/pod_list_b3.json +0 -75
  45. data/test/json/replication_controller_b3.json +0 -57
  46. data/test/json/replication_controller_list_b3.json +0 -64
  47. data/test/json/secret_list_b3.json +0 -44
  48. data/test/json/service_b3.json +0 -33
  49. data/test/json/service_illegal_json_404.json +0 -1
  50. data/test/json/service_list_b3.json +0 -97
  51. data/test/json/service_update_b3.json +0 -22
  52. data/test/json/versions_list.json +0 -6
  53. data/test/json/watch_stream_b3.json +0 -3
  54. data/test/test_helper.rb +0 -4
  55. data/test/test_kubeclient.rb +0 -407
  56. data/test/test_namespace.rb +0 -53
  57. data/test/test_node.rb +0 -25
  58. data/test/test_pod.rb +0 -21
  59. data/test/test_replication_controller.rb +0 -24
  60. data/test/test_secret.rb +0 -58
  61. data/test/test_service.rb +0 -136
  62. data/test/test_watch.rb +0 -37
  63. data/test/valid_token_file +0 -1
@@ -1,225 +1,661 @@
1
1
  require 'json'
2
2
  require 'rest-client'
3
+
3
4
  module Kubeclient
4
- module Common
5
- # Common methods
6
- class Client
7
- def handle_exception
8
- yield
9
- rescue RestClient::Exception => e
10
- begin
11
- json_error_msg = JSON.parse(e.response || '') || {}
12
- rescue JSON::ParserError
13
- json_error_msg = {}
14
- end
15
- err_message = json_error_msg['message'] || e.message
16
- raise KubeException.new(e.http_code, err_message)
5
+ # Common methods
6
+ # this is mixed in by other gems
7
+ module ClientMixin
8
+ ENTITY_METHODS = %w[get watch delete create update patch json_patch merge_patch apply].freeze
9
+
10
+ DEFAULT_SSL_OPTIONS = {
11
+ client_cert: nil,
12
+ client_key: nil,
13
+ ca_file: nil,
14
+ cert_store: nil,
15
+ verify_ssl: OpenSSL::SSL::VERIFY_PEER
16
+ }.freeze
17
+
18
+ DEFAULT_AUTH_OPTIONS = {
19
+ username: nil,
20
+ password: nil,
21
+ bearer_token: nil,
22
+ bearer_token_file: nil
23
+ }.freeze
24
+
25
+ DEFAULT_SOCKET_OPTIONS = {
26
+ socket_class: nil,
27
+ ssl_socket_class: nil
28
+ }.freeze
29
+
30
+ DEFAULT_TIMEOUTS = {
31
+ # These do NOT affect watch, watching never times out.
32
+ open: Net::HTTP.new('127.0.0.1').open_timeout, # depends on ruby version
33
+ read: Net::HTTP.new('127.0.0.1').read_timeout
34
+ }.freeze
35
+
36
+ DEFAULT_HTTP_PROXY_URI = nil
37
+ DEFAULT_HTTP_MAX_REDIRECTS = 10
38
+
39
+ SEARCH_ARGUMENTS = {
40
+ 'labelSelector' => :label_selector,
41
+ 'fieldSelector' => :field_selector,
42
+ 'resourceVersion' => :resource_version,
43
+ 'limit' => :limit,
44
+ 'continue' => :continue
45
+ }.freeze
46
+
47
+ WATCH_ARGUMENTS = {
48
+ 'labelSelector' => :label_selector,
49
+ 'fieldSelector' => :field_selector,
50
+ 'resourceVersion' => :resource_version
51
+ }.freeze
52
+
53
+ attr_reader :api_endpoint
54
+ attr_reader :ssl_options
55
+ attr_reader :auth_options
56
+ attr_reader :http_proxy_uri
57
+ attr_reader :http_max_redirects
58
+ attr_reader :headers
59
+ attr_reader :discovered
60
+
61
+ def initialize_client(
62
+ uri,
63
+ path,
64
+ version,
65
+ ssl_options: DEFAULT_SSL_OPTIONS,
66
+ auth_options: DEFAULT_AUTH_OPTIONS,
67
+ socket_options: DEFAULT_SOCKET_OPTIONS,
68
+ timeouts: DEFAULT_TIMEOUTS,
69
+ http_proxy_uri: DEFAULT_HTTP_PROXY_URI,
70
+ http_max_redirects: DEFAULT_HTTP_MAX_REDIRECTS,
71
+ as: :ros
72
+ )
73
+ validate_auth_options(auth_options)
74
+ handle_uri(uri, path)
75
+
76
+ @entities = {}
77
+ @discovered = false
78
+ @api_version = version
79
+ @headers = {}
80
+ @ssl_options = ssl_options
81
+ @auth_options = auth_options
82
+ @socket_options = socket_options
83
+ # Allow passing partial timeouts hash, without unspecified
84
+ # @timeouts[:foo] == nil resulting in infinite timeout.
85
+ @timeouts = DEFAULT_TIMEOUTS.merge(timeouts)
86
+ @http_proxy_uri = http_proxy_uri ? http_proxy_uri.to_s : nil
87
+ @http_max_redirects = http_max_redirects
88
+ @as = as
89
+
90
+ if auth_options[:bearer_token]
91
+ bearer_token(@auth_options[:bearer_token])
92
+ elsif auth_options[:bearer_token_file]
93
+ validate_bearer_token_file
94
+ bearer_token(File.read(@auth_options[:bearer_token_file]))
17
95
  end
96
+ end
18
97
 
19
- def handle_uri(uri, path)
20
- @api_endpoint = (uri.is_a? URI) ? uri : URI.parse(uri)
21
- @api_endpoint.path = path if @api_endpoint.path.empty?
22
- @api_endpoint.path = @api_endpoint.path.chop \
23
- if @api_endpoint.path.end_with? '/'
98
+ def method_missing(method_sym, *args, &block)
99
+ if discovery_needed?(method_sym)
100
+ discover
101
+ send(method_sym, *args, &block)
102
+ else
103
+ super
24
104
  end
105
+ end
25
106
 
26
- def build_namespace_prefix(namespace)
27
- namespace.to_s.empty? ? '' : "namespaces/#{namespace}/"
107
+ def respond_to_missing?(method_sym, include_private = false)
108
+ if discovery_needed?(method_sym)
109
+ discover
110
+ respond_to?(method_sym, include_private)
111
+ else
112
+ super
28
113
  end
114
+ end
29
115
 
30
- public
31
-
32
- def self.define_entity_methods(entity_types)
33
- entity_types.each do |klass, entity_type|
34
- entity_name = entity_type.underscore
35
- entity_name_plural = entity_name.pluralize
36
-
37
- # get all entities of a type e.g. get_nodes, get_pods, etc.
38
- define_method("get_#{entity_name_plural}") do |options = {}|
39
- get_entities(entity_type, klass, options)
40
- end
116
+ def discovery_needed?(method_sym)
117
+ !@discovered && ENTITY_METHODS.any? { |x| method_sym.to_s.start_with?(x) }
118
+ end
41
119
 
42
- # watch all entities of a type e.g. watch_nodes, watch_pods, etc.
43
- define_method("watch_#{entity_name_plural}") \
44
- do |resource_version = nil|
45
- watch_entities(entity_type, resource_version)
46
- end
120
+ def handle_exception
121
+ yield
122
+ rescue RestClient::Exception => e
123
+ json_error_msg = begin
124
+ JSON.parse(e.response || '') || {}
125
+ rescue JSON::ParserError
126
+ {}
127
+ end
128
+ err_message = json_error_msg['message'] || e.message
129
+ error_klass = e.http_code == 404 ? ResourceNotFoundError : HttpError
130
+ raise error_klass.new(e.http_code, err_message, e.response)
131
+ end
47
132
 
48
- # get a single entity of a specific type by name
49
- define_method("get_#{entity_name}") do |name, namespace = nil|
50
- get_entity(entity_type, klass, name, namespace)
51
- end
133
+ def discover
134
+ load_entities
135
+ define_entity_methods
136
+ @discovered = true
137
+ end
52
138
 
53
- define_method("delete_#{entity_name}") do |name, namespace = nil|
54
- delete_entity(entity_type, name, namespace)
139
+ def self.parse_definition(kind, name)
140
+ # Kubernetes gives us 3 inputs:
141
+ # kind: "ComponentStatus", "NetworkPolicy", "Endpoints"
142
+ # name: "componentstatuses", "networkpolicies", "endpoints"
143
+ # singularName: "componentstatus" etc (usually omitted, defaults to kind.downcase)
144
+ # and want to derive singular and plural method names, with underscores:
145
+ # "network_policy"
146
+ # "network_policies"
147
+ # kind's CamelCase word boundaries determine our placement of underscores.
148
+
149
+ if IRREGULAR_NAMES[kind]
150
+ # In a few cases, the given kind / singularName itself is still plural.
151
+ # We require a distinct singular method name, so force it.
152
+ method_names = IRREGULAR_NAMES[kind]
153
+ else
154
+ # TODO: respect singularName from discovery?
155
+ # But how? If it differs from kind.downcase, kind's word boundaries don't apply.
156
+ singular_name = kind.downcase
157
+
158
+ if !(/[A-Z]/ =~ kind)
159
+ # Some custom resources have a fully lowercase kind - can't infer underscores.
160
+ method_names = [singular_name, name]
161
+ else
162
+ # Some plurals are not exact suffixes, e.g. NetworkPolicy -> networkpolicies.
163
+ # So don't expect full last word to match.
164
+ /^(?<prefix>(.*[A-Z]))(?<singular_suffix>[^A-Z]*)$/ =~ kind # "NetworkP", "olicy"
165
+ if name.start_with?(prefix.downcase)
166
+ plural_suffix = name[prefix.length..-1] # "olicies"
167
+ prefix_underscores = ClientMixin.underscore_entity(prefix) # "network_p"
168
+ method_names = [prefix_underscores + singular_suffix, # "network_policy"
169
+ prefix_underscores + plural_suffix] # "network_policies"
170
+ else
171
+ method_names = resolve_unconventional_method_names(name, kind, singular_name)
55
172
  end
173
+ end
174
+ end
56
175
 
57
- define_method("create_#{entity_name}") do |entity_config|
58
- create_entity(entity_type, entity_config, klass)
59
- end
176
+ OpenStruct.new(
177
+ entity_type: kind,
178
+ resource_name: name,
179
+ method_names: method_names
180
+ )
181
+ end
60
182
 
61
- define_method("update_#{entity_name}") do |entity_config|
62
- update_entity(entity_type, entity_config)
63
- end
64
- end
183
+ def self.resolve_unconventional_method_names(name, kind, singular_name)
184
+ underscored_name = name.tr('-', '_')
185
+ singular_underscores = ClientMixin.underscore_entity(kind)
186
+ if underscored_name.start_with?(singular_underscores)
187
+ [singular_underscores, underscored_name]
188
+ else
189
+ # fallback to lowercase, no separators for both names
190
+ [singular_name, underscored_name.tr('_', '')]
65
191
  end
192
+ end
66
193
 
67
- def create_rest_client(path = nil)
68
- path ||= @api_endpoint.path
69
- options = {
70
- ssl_ca_file: @ssl_options[:ca_file],
71
- verify_ssl: @ssl_options[:verify_ssl],
72
- ssl_client_cert: @ssl_options[:client_cert],
73
- ssl_client_key: @ssl_options[:client_key],
74
- user: @basic_auth_user,
75
- password: @basic_auth_password
76
- }
77
- RestClient::Resource.new(@api_endpoint.merge(path).to_s, options)
194
+ def handle_uri(uri, path)
195
+ raise ArgumentError, 'Missing uri' unless uri
196
+ @api_endpoint = (uri.is_a?(URI) ? uri : URI.parse(uri))
197
+
198
+ # This regex will anchor at the last `/api`, `/oapi` or`/apis/:group`) part of the URL
199
+ # The whole path will be matched and if existing, the api_group will be extracted.
200
+ re = /^(?<path>.*\/o?api(?:s\/(?<apigroup>[^\/]+))?)$/mi
201
+ match = re.match(@api_endpoint.path.chomp('/'))
202
+
203
+ if match
204
+ # Since `re` captures 2 groups, match will always have 3 elements
205
+ # If thus we have a non-nil value in match 2, this is our api_group.
206
+ @api_group = match[:apigroup].nil? ? '' : match[:apigroup] + '/'
207
+ @api_endpoint.path = match[:path]
208
+ else
209
+ # This is a fallback, for when `/api` was not provided as part of the uri
210
+ @api_group = ''
211
+ @api_endpoint.path = @api_endpoint.path.chomp('/') + path
78
212
  end
213
+ end
79
214
 
80
- def rest_client
81
- @rest_client ||= begin
82
- create_rest_client("#{@api_endpoint.path}/#{@api_version}")
215
+ def build_namespace_prefix(namespace)
216
+ namespace.to_s.empty? ? '' : "namespaces/#{namespace}/"
217
+ end
218
+
219
+ # rubocop:disable Metrics/BlockLength
220
+ def define_entity_methods
221
+ @entities.values.each do |entity|
222
+ # get all entities of a type e.g. get_nodes, get_pods, etc.
223
+ define_singleton_method("get_#{entity.method_names[1]}") do |options = {}|
224
+ get_entities(entity.entity_type, entity.resource_name, options)
83
225
  end
84
- end
85
226
 
86
- def watch_entities(entity_type, resource_version = nil)
87
- resource = resource_name(entity_type.to_s)
227
+ # watch all entities of a type e.g. watch_nodes, watch_pods, etc.
228
+ define_singleton_method("watch_#{entity.method_names[1]}") do |options = {}, &block|
229
+ # This method used to take resource_version as a param, so
230
+ # this conversion is to keep backwards compatibility
231
+ options = { resource_version: options } unless options.is_a?(Hash)
88
232
 
89
- uri = @api_endpoint
90
- .merge("#{@api_endpoint.path}/#{@api_version}/watch/#{resource}")
233
+ watch_entities(entity.resource_name, options, &block)
234
+ end
91
235
 
92
- unless resource_version.nil?
93
- uri.query = URI.encode_www_form('resourceVersion' => resource_version)
236
+ # get a single entity of a specific type by name
237
+ define_singleton_method("get_#{entity.method_names[0]}") \
238
+ do |name, namespace = nil, opts = {}|
239
+ get_entity(entity.resource_name, name, namespace, opts)
94
240
  end
95
241
 
96
- options = {
97
- use_ssl: uri.scheme == 'https',
98
- ca_file: @ssl_options[:ca_file],
99
- # ruby Net::HTTP uses verify_mode instead of verify_ssl
100
- # http://ruby-doc.org/stdlib-1.9.3/libdoc/net/http/rdoc/Net/HTTP.html
101
- verify_mode: @ssl_options[:verify_ssl],
102
- cert: @ssl_options[:client_cert],
103
- key: @ssl_options[:client_key],
104
- basic_auth_user: @basic_auth_user,
105
- basic_auth_password: @basic_auth_password,
106
- headers: @headers
107
- }
242
+ define_singleton_method("delete_#{entity.method_names[0]}") \
243
+ do |name, namespace = nil, opts = {}|
244
+ delete_entity(entity.resource_name, name, namespace, **opts)
245
+ end
108
246
 
109
- WatchStream.new(uri, options)
110
- end
247
+ define_singleton_method("create_#{entity.method_names[0]}") do |entity_config|
248
+ create_entity(entity.entity_type, entity.resource_name, entity_config)
249
+ end
111
250
 
112
- def get_entities(entity_type, klass, options)
113
- params = {}
114
- if options[:label_selector]
115
- params['params'] = { labelSelector: options[:label_selector] }
251
+ define_singleton_method("update_#{entity.method_names[0]}") do |entity_config|
252
+ update_entity(entity.resource_name, entity_config)
116
253
  end
117
254
 
118
- # TODO: namespace support?
119
- response = handle_exception do
120
- rest_client[resource_name(entity_type)]
121
- .get(params.merge(@headers))
255
+ define_singleton_method("patch_#{entity.method_names[0]}") \
256
+ do |name, patch, namespace = nil|
257
+ patch_entity(entity.resource_name, name, patch, 'strategic-merge-patch', namespace)
122
258
  end
123
259
 
124
- result = JSON.parse(response)
260
+ define_singleton_method("json_patch_#{entity.method_names[0]}") \
261
+ do |name, patch, namespace = nil|
262
+ patch_entity(entity.resource_name, name, patch, 'json-patch', namespace)
263
+ end
125
264
 
126
- resource_version = result.fetch('resourceVersion', nil)
127
- if resource_version.nil?
128
- resource_version =
129
- result.fetch('metadata', {}).fetch('resourceVersion', nil)
265
+ define_singleton_method("merge_patch_#{entity.method_names[0]}") \
266
+ do |name, patch, namespace = nil|
267
+ patch_entity(entity.resource_name, name, patch, 'merge-patch', namespace)
130
268
  end
131
269
 
132
- collection = result['items'].map { |item| new_entity(item, klass) }
270
+ define_singleton_method("apply_#{entity.method_names[0]}") do |resource, opts = {}|
271
+ apply_entity(entity.resource_name, resource, **opts)
272
+ end
273
+ end
274
+ end
275
+ # rubocop:enable Metrics/BlockLength
133
276
 
134
- EntityList.new(entity_type, resource_version, collection)
277
+ def self.underscore_entity(entity_name)
278
+ entity_name.gsub(/([a-z])([A-Z])/, '\1_\2').downcase
279
+ end
280
+
281
+ def create_rest_client(path = nil)
282
+ path ||= @api_endpoint.path
283
+ options = {
284
+ ssl_ca_file: @ssl_options[:ca_file],
285
+ ssl_cert_store: @ssl_options[:cert_store],
286
+ verify_ssl: @ssl_options[:verify_ssl],
287
+ ssl_client_cert: @ssl_options[:client_cert],
288
+ ssl_client_key: @ssl_options[:client_key],
289
+ proxy: @http_proxy_uri,
290
+ max_redirects: @http_max_redirects,
291
+ user: @auth_options[:username],
292
+ password: @auth_options[:password],
293
+ open_timeout: @timeouts[:open],
294
+ read_timeout: @timeouts[:read]
295
+ }
296
+ RestClient::Resource.new(@api_endpoint.merge(path).to_s, options)
297
+ end
298
+
299
+ def rest_client
300
+ @rest_client ||= begin
301
+ create_rest_client("#{@api_endpoint.path}/#{@api_version}")
135
302
  end
303
+ end
304
+
305
+ # Accepts the following options:
306
+ # :namespace (string) - the namespace of the entity.
307
+ # :name (string) - the name of the entity to watch.
308
+ # :label_selector (string) - a selector to restrict the list of returned objects by labels.
309
+ # :field_selector (string) - a selector to restrict the list of returned objects by fields.
310
+ # :resource_version (string) - shows changes that occur after passed version of a resource.
311
+ # :as (:raw|:ros) - defaults to :ros
312
+ # :raw - return the raw response body as a string
313
+ # :ros - return a collection of RecursiveOpenStruct objects
314
+ # Accepts an optional block, that will be called with each entity,
315
+ # otherwise returns a WatchStream
316
+ def watch_entities(resource_name, options = {}, &block)
317
+ ns = build_namespace_prefix(options[:namespace])
318
+
319
+ path = "watch/#{ns}#{resource_name}"
320
+ path += "/#{options[:name]}" if options[:name]
321
+ uri = @api_endpoint.merge("#{@api_endpoint.path}/#{@api_version}/#{path}")
322
+
323
+ params = {}
324
+ WATCH_ARGUMENTS.each { |k, v| params[k] = options[v] if options[v] }
325
+ uri.query = URI.encode_www_form(params) if params.any?
326
+
327
+ watcher = Kubeclient::Common::WatchStream.new(
328
+ uri,
329
+ http_options(uri),
330
+ formatter: ->(value) { format_response(options[:as] || @as, value) }
331
+ )
332
+
333
+ return_or_yield_to_watcher(watcher, &block)
334
+ end
136
335
 
137
- def get_entity(entity_type, klass, name, namespace = nil)
138
- ns_prefix = build_namespace_prefix(namespace)
139
- response = handle_exception do
140
- rest_client[ns_prefix + resource_name(entity_type) + "/#{name}"]
336
+ # Accepts the following options:
337
+ # :namespace (string) - the namespace of the entity.
338
+ # :label_selector (string) - a selector to restrict the list of returned objects by labels.
339
+ # :field_selector (string) - a selector to restrict the list of returned objects by fields.
340
+ # :limit (integer) - a maximum number of items to return in each response
341
+ # :continue (string) - a token used to retrieve the next chunk of entities
342
+ # :as (:raw|:ros) - defaults to :ros
343
+ # :raw - return the raw response body as a string
344
+ # :ros - return a collection of RecursiveOpenStruct objects
345
+ def get_entities(entity_type, resource_name, options = {})
346
+ params = {}
347
+ SEARCH_ARGUMENTS.each { |k, v| params[k] = options[v] if options[v] }
348
+
349
+ ns_prefix = build_namespace_prefix(options[:namespace])
350
+ response = handle_exception do
351
+ rest_client[ns_prefix + resource_name]
352
+ .get({ 'params' => params }.merge(@headers))
353
+ end
354
+ format_response(options[:as] || @as, response.body, entity_type)
355
+ end
356
+
357
+ # Accepts the following options:
358
+ # :as (:raw|:ros) - defaults to :ros
359
+ # :raw - return the raw response body as a string
360
+ # :ros - return a collection of RecursiveOpenStruct objects
361
+ def get_entity(resource_name, name, namespace = nil, options = {})
362
+ ns_prefix = build_namespace_prefix(namespace)
363
+ response = handle_exception do
364
+ rest_client[ns_prefix + resource_name + "/#{name}"]
141
365
  .get(@headers)
142
- end
143
- result = JSON.parse(response)
144
- new_entity(result, klass)
145
366
  end
367
+ format_response(options[:as] || @as, response.body)
368
+ end
146
369
 
147
- def delete_entity(entity_type, name, namespace = nil)
148
- ns_prefix = build_namespace_prefix(namespace)
149
- handle_exception do
150
- rest_client[ns_prefix + resource_name(entity_type) + "/#{name}"]
151
- .delete(@headers)
152
- end
370
+ # delete_options are passed as a JSON payload in the delete request
371
+ def delete_entity(resource_name, name, namespace = nil, delete_options: {})
372
+ delete_options_hash = delete_options.to_hash
373
+ ns_prefix = build_namespace_prefix(namespace)
374
+ payload = delete_options_hash.to_json unless delete_options_hash.empty?
375
+ response = handle_exception do
376
+ rs = rest_client[ns_prefix + resource_name + "/#{name}"]
377
+ RestClient::Request.execute(
378
+ rs.options.merge(
379
+ method: :delete,
380
+ url: rs.url,
381
+ headers: { 'Content-Type' => 'application/json' }.merge(@headers),
382
+ payload: payload
383
+ )
384
+ )
153
385
  end
386
+ format_response(@as, response.body)
387
+ end
154
388
 
155
- def create_entity(entity_type, entity_config, klass)
156
- # to_hash should be called because of issue #9 in recursive open
157
- # struct
158
- hash = entity_config.to_hash
389
+ def create_entity(entity_type, resource_name, entity_config)
390
+ # Duplicate the entity_config to a hash so that when we assign
391
+ # kind and apiVersion, this does not mutate original entity_config obj.
392
+ hash = entity_config.to_hash
393
+
394
+ ns_prefix = build_namespace_prefix(hash[:metadata][:namespace])
395
+
396
+ # TODO: temporary solution to add "kind" and apiVersion to request
397
+ # until this issue is solved
398
+ # https://github.com/GoogleCloudPlatform/kubernetes/issues/6439
399
+ hash[:kind] = entity_type
400
+ hash[:apiVersion] = @api_group + @api_version
401
+ response = handle_exception do
402
+ rest_client[ns_prefix + resource_name]
403
+ .post(hash.to_json, { 'Content-Type' => 'application/json' }.merge(@headers))
404
+ end
405
+ format_response(@as, response.body)
406
+ end
159
407
 
160
- ns_prefix = build_namespace_prefix(entity_config.metadata.namespace)
408
+ def update_entity(resource_name, entity_config)
409
+ name = entity_config[:metadata][:name]
410
+ ns_prefix = build_namespace_prefix(entity_config[:metadata][:namespace])
411
+ response = handle_exception do
412
+ rest_client[ns_prefix + resource_name + "/#{name}"]
413
+ .put(entity_config.to_h.to_json, { 'Content-Type' => 'application/json' }.merge(@headers))
414
+ end
415
+ format_response(@as, response.body)
416
+ end
161
417
 
162
- # TODO: temporary solution to add "kind" and apiVersion to request
163
- # until this issue is solved
164
- # https://github.com/GoogleCloudPlatform/kubernetes/issues/6439
165
- hash['kind'] = entity_type
166
- hash['apiVersion'] = @api_version
167
- response = handle_exception do
168
- rest_client[ns_prefix + resource_name(entity_type)]
169
- .post(hash.to_json, @headers)
170
- end
171
- result = JSON.parse(response)
172
- new_entity(result, klass)
173
- end
174
-
175
- def update_entity(entity_type, entity_config)
176
- name = entity_config.metadata.name
177
- # to_hash should be called because of issue #9 in recursive open
178
- # struct
179
- hash = entity_config.to_hash
180
- ns_prefix = build_namespace_prefix(entity_config.metadata.namespace)
181
- handle_exception do
182
- rest_client[ns_prefix + resource_name(entity_type) + "/#{name}"]
183
- .put(hash.to_json, @headers)
418
+ def patch_entity(resource_name, name, patch, strategy, namespace)
419
+ ns_prefix = build_namespace_prefix(namespace)
420
+ response = handle_exception do
421
+ rest_client[ns_prefix + resource_name + "/#{name}"]
422
+ .patch(
423
+ patch.to_json,
424
+ { 'Content-Type' => "application/#{strategy}+json" }.merge(@headers)
425
+ )
426
+ end
427
+ format_response(@as, response.body)
428
+ end
429
+
430
+ def apply_entity(resource_name, resource, field_manager:, force: true)
431
+ name = "#{resource[:metadata][:name]}?fieldManager=#{field_manager}&force=#{force}"
432
+ ns_prefix = build_namespace_prefix(resource[:metadata][:namespace])
433
+ response = handle_exception do
434
+ rest_client[ns_prefix + resource_name + "/#{name}"]
435
+ .patch(
436
+ resource.to_json,
437
+ { 'Content-Type' => 'application/apply-patch+yaml' }.merge(@headers)
438
+ )
439
+ end
440
+ format_response(@as, response.body)
441
+ end
442
+
443
+ def all_entities(options = {})
444
+ discover unless @discovered
445
+ @entities.values.each_with_object({}) do |entity, result_hash|
446
+ # method call for get each entities
447
+ # build hash of entity name to array of the entities
448
+ method_name = "get_#{entity.method_names[1]}"
449
+ begin
450
+ result_hash[entity.method_names[0]] = send(method_name, options)
451
+ rescue Kubeclient::HttpError
452
+ next # do not fail due to resources not supporting get
184
453
  end
185
454
  end
455
+ end
186
456
 
187
- def new_entity(hash, klass)
188
- klass.new(hash)
457
+ def get_pod_log(pod_name, namespace,
458
+ container: nil, previous: false,
459
+ timestamps: false, since_time: nil, tail_lines: nil, limit_bytes: nil)
460
+ params = {}
461
+ params[:previous] = true if previous
462
+ params[:container] = container if container
463
+ params[:timestamps] = timestamps if timestamps
464
+ params[:sinceTime] = format_datetime(since_time) if since_time
465
+ params[:tailLines] = tail_lines if tail_lines
466
+ params[:limitBytes] = limit_bytes if limit_bytes
467
+
468
+ ns = build_namespace_prefix(namespace)
469
+ handle_exception do
470
+ rest_client[ns + "pods/#{pod_name}/log"]
471
+ .get({ 'params' => params }.merge(@headers))
189
472
  end
473
+ end
474
+
475
+ def watch_pod_log(pod_name, namespace, container: nil, &block)
476
+ # Adding the "follow=true" query param tells the Kubernetes API to keep
477
+ # the connection open and stream updates to the log.
478
+ params = { follow: true }
479
+ params[:container] = container if container
480
+
481
+ ns = build_namespace_prefix(namespace)
482
+
483
+ uri = @api_endpoint.dup
484
+ uri.path += "/#{@api_version}/#{ns}pods/#{pod_name}/log"
485
+ uri.query = URI.encode_www_form(params)
190
486
 
191
- def retrieve_all_entities(entity_types)
192
- entity_types.each_with_object({}) do |(_, entity_type), result_hash|
193
- # method call for get each entities
194
- # build hash of entity name to array of the entities
195
- method_name = "get_#{entity_type.underscore.pluralize}"
196
- key_name = entity_type.underscore
197
- result_hash[key_name] = send(method_name)
487
+ watcher = Kubeclient::Common::WatchStream.new(
488
+ uri, http_options(uri), formatter: ->(value) { value }
489
+ )
490
+ return_or_yield_to_watcher(watcher, &block)
491
+ end
492
+
493
+ def proxy_url(kind, name, port, namespace = '')
494
+ discover unless @discovered
495
+ entity_name_plural =
496
+ if %w[services pods nodes].include?(kind.to_s)
497
+ kind.to_s
498
+ else
499
+ @entities[kind.to_s].resource_name
198
500
  end
501
+ ns_prefix = build_namespace_prefix(namespace)
502
+ rest_client["#{ns_prefix}#{entity_name_plural}/#{name}:#{port}/proxy"].url
503
+ end
504
+
505
+ def process_template(template)
506
+ ns_prefix = build_namespace_prefix(template[:metadata][:namespace])
507
+ response = handle_exception do
508
+ rest_client[ns_prefix + 'processedtemplates']
509
+ .post(template.to_h.to_json, { 'Content-Type' => 'application/json' }.merge(@headers))
199
510
  end
511
+ JSON.parse(response)
512
+ end
200
513
 
201
- def resource_name(entity_type)
202
- entity_type.pluralize.downcase
514
+ def api_valid?
515
+ result = api
516
+ result.is_a?(Hash) && (result['versions'] || []).any? do |group|
517
+ @api_group.empty? ? group.include?(@api_version) : group['version'] == @api_version
203
518
  end
519
+ end
204
520
 
205
- def api_valid?
206
- result = api
207
- result.is_a?(Hash) && (result['versions'] || []).include?(@api_version)
521
+ def api
522
+ response = handle_exception { create_rest_client.get(@headers) }
523
+ JSON.parse(response)
524
+ end
525
+
526
+ private
527
+
528
+ IRREGULAR_NAMES = {
529
+ # In a few cases, the given kind itself is still plural.
530
+ # https://github.com/kubernetes/kubernetes/issues/8115
531
+ 'Endpoints' => %w[endpoint endpoints],
532
+ 'SecurityContextConstraints' => %w[security_context_constraint
533
+ security_context_constraints]
534
+ }.freeze
535
+
536
+ # Format datetime according to RFC3339
537
+ def format_datetime(value)
538
+ case value
539
+ when DateTime, Time
540
+ value.strftime('%FT%T.%9N%:z')
541
+ when String
542
+ value
543
+ else
544
+ raise ArgumentError, "unsupported type '#{value.class}' of time value '#{value}'"
208
545
  end
546
+ end
209
547
 
210
- def api
211
- response = handle_exception do
212
- create_rest_client.get(@headers)
548
+ def format_response(as, body, list_type = nil)
549
+ case as
550
+ when :raw
551
+ body
552
+ when :parsed
553
+ JSON.parse(body)
554
+ when :parsed_symbolized
555
+ JSON.parse(body, symbolize_names: true)
556
+ when :ros
557
+ result = JSON.parse(body)
558
+
559
+ if list_type
560
+ resource_version =
561
+ result.fetch('resourceVersion') do
562
+ result.fetch('metadata', {}).fetch('resourceVersion', nil)
563
+ end
564
+
565
+ # If 'limit' was passed save the continue token
566
+ # see https://kubernetes.io/docs/reference/using-api/api-concepts/#retrieving-large-results-sets-in-chunks
567
+ continue = result.fetch('metadata', {}).fetch('continue', nil)
568
+
569
+ # result['items'] might be nil due to https://github.com/kubernetes/kubernetes/issues/13096
570
+ collection = result['items'].to_a.map { |item| Kubeclient::Resource.new(item) }
571
+
572
+ Kubeclient::Common::EntityList.new(list_type, resource_version, collection, continue)
573
+ else
574
+ Kubeclient::Resource.new(result)
213
575
  end
214
- JSON.parse(response)
576
+ else
577
+ raise ArgumentError, "Unsupported format #{as.inspect}"
578
+ end
579
+ end
580
+
581
+ def load_entities
582
+ @entities = {}
583
+ fetch_entities['resources'].each do |resource|
584
+ next if resource['name'].include?('/')
585
+ # Not a regular entity, special functionality covered by `process_template`.
586
+ # https://github.com/openshift/origin/issues/21668
587
+ next if resource['kind'] == 'Template' && resource['name'] == 'processedtemplates'
588
+ resource['kind'] ||=
589
+ Kubeclient::Common::MissingKindCompatibility.resource_kind(resource['name'])
590
+ entity = ClientMixin.parse_definition(resource['kind'], resource['name'])
591
+ @entities[entity.method_names[0]] = entity if entity
215
592
  end
593
+ end
216
594
 
217
- private
595
+ def fetch_entities
596
+ JSON.parse(handle_exception { rest_client.get(@headers) })
597
+ end
218
598
 
219
- def bearer_token(bearer_token)
220
- @headers ||= {}
221
- @headers[:Authorization] = "Bearer #{bearer_token}"
599
+ def bearer_token(bearer_token)
600
+ @headers ||= {}
601
+ @headers[:Authorization] = "Bearer #{bearer_token}"
602
+ end
603
+
604
+ def validate_auth_options(opts)
605
+ # maintain backward compatibility:
606
+ opts[:username] = opts[:user] if opts[:user]
607
+
608
+ if %i[bearer_token bearer_token_file username].count { |key| opts[key] } > 1
609
+ raise(
610
+ ArgumentError,
611
+ 'Invalid auth options: specify only one of username/password,' \
612
+ ' bearer_token or bearer_token_file'
613
+ )
614
+ elsif %i[username password].count { |key| opts[key] } == 1
615
+ raise ArgumentError, 'Basic auth requires both username & password'
222
616
  end
223
617
  end
618
+
619
+ def validate_bearer_token_file
620
+ msg = "Token file #{@auth_options[:bearer_token_file]} does not exist"
621
+ raise ArgumentError, msg unless File.file?(@auth_options[:bearer_token_file])
622
+
623
+ msg = "Cannot read token file #{@auth_options[:bearer_token_file]}"
624
+ raise ArgumentError, msg unless File.readable?(@auth_options[:bearer_token_file])
625
+ end
626
+
627
+ def return_or_yield_to_watcher(watcher, &block)
628
+ return watcher unless block_given?
629
+
630
+ begin
631
+ watcher.each(&block)
632
+ ensure
633
+ watcher.finish
634
+ end
635
+ end
636
+
637
+ def http_options(uri)
638
+ options = {
639
+ basic_auth_user: @auth_options[:username],
640
+ basic_auth_password: @auth_options[:password],
641
+ headers: @headers,
642
+ http_proxy_uri: @http_proxy_uri,
643
+ http_max_redirects: http_max_redirects
644
+ }
645
+
646
+ if uri.scheme == 'https'
647
+ options[:ssl] = {
648
+ ca_file: @ssl_options[:ca_file],
649
+ cert: @ssl_options[:client_cert],
650
+ cert_store: @ssl_options[:cert_store],
651
+ key: @ssl_options[:client_key],
652
+ # ruby HTTP uses verify_mode instead of verify_ssl
653
+ # http://ruby-doc.org/stdlib-1.9.3/libdoc/openssl/rdoc/OpenSSL/SSL/SSLContext.html
654
+ verify_mode: @ssl_options[:verify_ssl]
655
+ }
656
+ end
657
+
658
+ options.merge(@socket_options)
659
+ end
224
660
  end
225
661
  end