kubeclient-rollback-dev 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (97) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +15 -0
  3. data/.rubocop.yml +16 -0
  4. data/.travis.yml +12 -0
  5. data/Gemfile +4 -0
  6. data/LICENSE.txt +22 -0
  7. data/README.md +428 -0
  8. data/Rakefile +12 -0
  9. data/kubeclient.gemspec +29 -0
  10. data/lib/kubeclient/common.rb +512 -0
  11. data/lib/kubeclient/config.rb +126 -0
  12. data/lib/kubeclient/entity_list.rb +16 -0
  13. data/lib/kubeclient/kube_exception.rb +14 -0
  14. data/lib/kubeclient/missing_kind_compatibility.rb +68 -0
  15. data/lib/kubeclient/version.rb +4 -0
  16. data/lib/kubeclient/watch_notice.rb +7 -0
  17. data/lib/kubeclient/watch_stream.rb +80 -0
  18. data/lib/kubeclient.rb +32 -0
  19. data/test/cassettes/kubernetes_guestbook.yml +879 -0
  20. data/test/config/allinone.kubeconfig +20 -0
  21. data/test/config/external-ca.pem +18 -0
  22. data/test/config/external-cert.pem +19 -0
  23. data/test/config/external-key.rsa +27 -0
  24. data/test/config/external.kubeconfig +20 -0
  25. data/test/config/nouser.kubeconfig +16 -0
  26. data/test/config/userauth.kubeconfig +28 -0
  27. data/test/json/bindings_list.json +10 -0
  28. data/test/json/component_status.json +17 -0
  29. data/test/json/component_status_list.json +52 -0
  30. data/test/json/config_map_list.json +9 -0
  31. data/test/json/core_api_resource_list.json +181 -0
  32. data/test/json/core_api_resource_list_without_kind.json +129 -0
  33. data/test/json/core_oapi_resource_list_without_kind.json +197 -0
  34. data/test/json/created_endpoint.json +28 -0
  35. data/test/json/created_namespace.json +20 -0
  36. data/test/json/created_secret.json +16 -0
  37. data/test/json/created_service.json +31 -0
  38. data/test/json/empty_pod_list.json +9 -0
  39. data/test/json/endpoint_list.json +48 -0
  40. data/test/json/entity_list.json +56 -0
  41. data/test/json/event_list.json +35 -0
  42. data/test/json/limit_range.json +23 -0
  43. data/test/json/limit_range_list.json +31 -0
  44. data/test/json/namespace.json +13 -0
  45. data/test/json/namespace_exception.json +8 -0
  46. data/test/json/namespace_list.json +32 -0
  47. data/test/json/node.json +29 -0
  48. data/test/json/node_list.json +37 -0
  49. data/test/json/persistent_volume.json +37 -0
  50. data/test/json/persistent_volume_claim.json +32 -0
  51. data/test/json/persistent_volume_claim_list.json +40 -0
  52. data/test/json/persistent_volume_claims_nil_items.json +8 -0
  53. data/test/json/persistent_volume_list.json +45 -0
  54. data/test/json/pod.json +92 -0
  55. data/test/json/pod_list.json +79 -0
  56. data/test/json/pod_template_list.json +9 -0
  57. data/test/json/processed_template.json +27 -0
  58. data/test/json/replication_controller.json +57 -0
  59. data/test/json/replication_controller_list.json +66 -0
  60. data/test/json/resource_quota.json +46 -0
  61. data/test/json/resource_quota_list.json +54 -0
  62. data/test/json/secret_list.json +44 -0
  63. data/test/json/service.json +33 -0
  64. data/test/json/service_account.json +25 -0
  65. data/test/json/service_account_list.json +82 -0
  66. data/test/json/service_illegal_json_404.json +1 -0
  67. data/test/json/service_list.json +97 -0
  68. data/test/json/service_patch.json +25 -0
  69. data/test/json/service_update.json +22 -0
  70. data/test/json/versions_list.json +6 -0
  71. data/test/json/watch_stream.json +3 -0
  72. data/test/test_common.rb +32 -0
  73. data/test/test_component_status.rb +30 -0
  74. data/test/test_config.rb +72 -0
  75. data/test/test_endpoint.rb +35 -0
  76. data/test/test_guestbook_go.rb +238 -0
  77. data/test/test_helper.rb +10 -0
  78. data/test/test_kubeclient.rb +611 -0
  79. data/test/test_limit_range.rb +27 -0
  80. data/test/test_missing_methods.rb +42 -0
  81. data/test/test_namespace.rb +61 -0
  82. data/test/test_node.rb +33 -0
  83. data/test/test_persistent_volume.rb +30 -0
  84. data/test/test_persistent_volume_claim.rb +30 -0
  85. data/test/test_pod.rb +29 -0
  86. data/test/test_pod_log.rb +50 -0
  87. data/test/test_process_template.rb +44 -0
  88. data/test/test_replication_controller.rb +27 -0
  89. data/test/test_resource_list_without_kind.rb +78 -0
  90. data/test/test_resource_quota.rb +25 -0
  91. data/test/test_secret.rb +70 -0
  92. data/test/test_service.rb +293 -0
  93. data/test/test_service_account.rb +28 -0
  94. data/test/test_watch.rb +119 -0
  95. data/test/txt/pod_log.txt +6 -0
  96. data/test/valid_token_file +1 -0
  97. metadata +343 -0
@@ -0,0 +1,512 @@
1
+ require 'json'
2
+ require 'rest-client'
3
+ module Kubeclient
4
+ # Common methods
5
+ # this is mixed in by other gems
6
+ module ClientMixin
7
+ ENTITY_METHODS = %w(get watch delete create update patch rollback).freeze
8
+
9
+ DEFAULT_SSL_OPTIONS = {
10
+ client_cert: nil,
11
+ client_key: nil,
12
+ ca_file: nil,
13
+ cert_store: nil,
14
+ verify_ssl: OpenSSL::SSL::VERIFY_PEER
15
+ }.freeze
16
+
17
+ DEFAULT_AUTH_OPTIONS = {
18
+ username: nil,
19
+ password: nil,
20
+ bearer_token: nil,
21
+ bearer_token_file: nil
22
+ }.freeze
23
+
24
+ DEFAULT_SOCKET_OPTIONS = {
25
+ socket_class: nil,
26
+ ssl_socket_class: nil
27
+ }.freeze
28
+
29
+ DEFAULT_HTTP_PROXY_URI = nil
30
+
31
+ SEARCH_ARGUMENTS = {
32
+ 'labelSelector' => :label_selector,
33
+ 'fieldSelector' => :field_selector
34
+ }.freeze
35
+
36
+ WATCH_ARGUMENTS = { 'resourceVersion' => :resource_version }.merge!(SEARCH_ARGUMENTS).freeze
37
+
38
+ attr_reader :api_endpoint
39
+ attr_reader :ssl_options
40
+ attr_reader :auth_options
41
+ attr_reader :http_proxy_uri
42
+ attr_reader :headers
43
+ attr_reader :discovered
44
+
45
+ def initialize_client(
46
+ class_owner,
47
+ uri,
48
+ path,
49
+ version,
50
+ ssl_options: DEFAULT_SSL_OPTIONS,
51
+ auth_options: DEFAULT_AUTH_OPTIONS,
52
+ socket_options: DEFAULT_SOCKET_OPTIONS,
53
+ http_proxy_uri: DEFAULT_HTTP_PROXY_URI
54
+ )
55
+ validate_auth_options(auth_options)
56
+ handle_uri(uri, path)
57
+
58
+ @class_owner = class_owner
59
+ @entities = {}
60
+ @discovered = false
61
+ @api_version = version
62
+ @headers = {}
63
+ @ssl_options = ssl_options
64
+ @auth_options = auth_options
65
+ @socket_options = socket_options
66
+ @http_proxy_uri = http_proxy_uri.to_s if http_proxy_uri
67
+
68
+ if auth_options[:bearer_token]
69
+ bearer_token(@auth_options[:bearer_token])
70
+ elsif auth_options[:bearer_token_file]
71
+ validate_bearer_token_file
72
+ bearer_token(File.read(@auth_options[:bearer_token_file]))
73
+ end
74
+ end
75
+
76
+ def method_missing(method_sym, *args, &block)
77
+ if discovery_needed?(method_sym)
78
+ discover
79
+ send(method_sym, *args, &block)
80
+ else
81
+ super
82
+ end
83
+ end
84
+
85
+ def respond_to_missing?(method_sym, include_private = false)
86
+ if discovery_needed?(method_sym)
87
+ discover
88
+ respond_to?(method_sym, include_private)
89
+ else
90
+ super
91
+ end
92
+ end
93
+
94
+ def discovery_needed?(method_sym)
95
+ !@discovered && ENTITY_METHODS.any? { |x| method_sym.to_s.start_with?(x) }
96
+ end
97
+
98
+ def handle_exception
99
+ yield
100
+ rescue RestClient::Exception => e
101
+ json_error_msg = begin
102
+ JSON.parse(e.response || '') || {}
103
+ rescue JSON::ParserError
104
+ {}
105
+ end
106
+ err_message = json_error_msg['message'] || e.message
107
+ raise KubeException.new(e.http_code, err_message, e.response)
108
+ end
109
+
110
+ def discover
111
+ load_entities
112
+ define_entity_methods
113
+ @discovered = true
114
+ end
115
+
116
+ def self.parse_definition(kind, name)
117
+ # "name": "componentstatuses", networkpolicies, endpoints
118
+ # "kind": "ComponentStatus" NetworkPolicy, Endpoints
119
+ # maintain pre group api compatibility for endpoints.
120
+ # See: https://github.com/kubernetes/kubernetes/issues/8115
121
+ kind = 'Endpoint' if kind == 'Endpoints'
122
+
123
+ prefix = kind[0..kind.rindex(/[A-Z]/)] # NetworkP
124
+ m = name.match(/^#{prefix.downcase}(.*)$/)
125
+ m && OpenStruct.new(
126
+ entity_type: kind, # ComponentStatus
127
+ resource_name: name, # componentstatuses
128
+ method_names: [
129
+ ClientMixin.underscore_entity(kind), # component_status
130
+ ClientMixin.underscore_entity(prefix) + m[1] # component_statuses
131
+ ]
132
+ )
133
+ end
134
+
135
+ def handle_uri(uri, path)
136
+ raise ArgumentError, 'Missing uri' unless uri
137
+ @api_endpoint = (uri.is_a?(URI) ? uri : URI.parse(uri))
138
+ @api_endpoint.path = path if @api_endpoint.path.empty?
139
+ @api_endpoint.path = @api_endpoint.path.chop if @api_endpoint.path.end_with? '/'
140
+ components = @api_endpoint.path.to_s.split('/') # ["", "api"] or ["", "apis", batch]
141
+ @api_group = components.length > 2 ? components[2] + '/' : ''
142
+ end
143
+
144
+ def build_namespace_prefix(namespace)
145
+ namespace.to_s.empty? ? '' : "namespaces/#{namespace}/"
146
+ end
147
+
148
+ def self.resource_class(class_owner, entity_type)
149
+ if class_owner.const_defined?(entity_type, false)
150
+ class_owner.const_get(entity_type, false)
151
+ else
152
+ class_owner.const_set(
153
+ entity_type,
154
+ Class.new(RecursiveOpenStruct) do
155
+ def initialize(hash = nil, args = {})
156
+ args[:recurse_over_arrays] = true
157
+ super(hash, args)
158
+ end
159
+ end
160
+ )
161
+ end
162
+ end
163
+
164
+ def define_entity_methods
165
+ @entities.values.each do |entity|
166
+ klass = ClientMixin.resource_class(@class_owner, entity.entity_type)
167
+ # get all entities of a type e.g. get_nodes, get_pods, etc.
168
+ define_singleton_method("get_#{entity.method_names[1]}") do |options = {}|
169
+ get_entities(entity.entity_type, klass, entity.resource_name, options)
170
+ end
171
+
172
+ # watch all entities of a type e.g. watch_nodes, watch_pods, etc.
173
+ define_singleton_method("watch_#{entity.method_names[1]}") do |options = {}|
174
+ # This method used to take resource_version as a param, so
175
+ # this conversion is to keep backwards compatibility
176
+ options = { resource_version: options } unless options.is_a?(Hash)
177
+
178
+ watch_entities(entity.resource_name, options)
179
+ end
180
+
181
+ # get a single entity of a specific type by name
182
+ define_singleton_method("get_#{entity.method_names[0]}") do |name, namespace = nil|
183
+ get_entity(klass, entity.resource_name, name, namespace)
184
+ end
185
+
186
+ define_singleton_method("delete_#{entity.method_names[0]}") do |name, namespace = nil|
187
+ delete_entity(entity.resource_name, name, namespace)
188
+ end
189
+
190
+ define_singleton_method("create_#{entity.method_names[0]}") do |entity_config|
191
+ create_entity(entity.entity_type, entity.resource_name, entity_config, klass)
192
+ end
193
+
194
+ define_singleton_method("update_#{entity.method_names[0]}") do |entity_config|
195
+ update_entity(entity.resource_name, entity_config)
196
+ end
197
+
198
+ define_singleton_method("patch_#{entity.method_names[0]}") do |name, patch, namespace = nil|
199
+ patch_entity(entity.resource_name, name, patch, namespace)
200
+ end
201
+
202
+ define_singleton_method("rollback_#{entity.method_names[0]}") do |name, entity_config = {}, namespace = nil|
203
+ rollback_entity(entity.resource_name, name, entity_config: entity_config, namespace: namespace)
204
+ end
205
+ end
206
+ end
207
+
208
+ def self.underscore_entity(entity_name)
209
+ entity_name.gsub(/([a-z])([A-Z])/, '\1_\2').downcase
210
+ end
211
+
212
+ def create_rest_client(path = nil)
213
+ path ||= @api_endpoint.path
214
+ options = {
215
+ ssl_ca_file: @ssl_options[:ca_file],
216
+ ssl_cert_store: @ssl_options[:cert_store],
217
+ verify_ssl: @ssl_options[:verify_ssl],
218
+ ssl_client_cert: @ssl_options[:client_cert],
219
+ ssl_client_key: @ssl_options[:client_key],
220
+ proxy: @http_proxy_uri,
221
+ user: @auth_options[:username],
222
+ password: @auth_options[:password]
223
+ }
224
+ RestClient::Resource.new(@api_endpoint.merge(path).to_s, options)
225
+ end
226
+
227
+ def rest_client
228
+ @rest_client ||= begin
229
+ create_rest_client("#{@api_endpoint.path}/#{@api_version}")
230
+ end
231
+ end
232
+
233
+ # Accepts the following string options:
234
+ # :namespace - the namespace of the entity.
235
+ # :name - the name of the entity to watch.
236
+ # :label_selector - a selector to restrict the list of returned objects by their labels.
237
+ # :field_selector - a selector to restrict the list of returned objects by their fields.
238
+ # :resource_version - shows changes that occur after that particular version of a resource.
239
+ def watch_entities(resource_name, options = {})
240
+ ns = build_namespace_prefix(options[:namespace])
241
+
242
+ path = "watch/#{ns}#{resource_name}"
243
+ path += "/#{options[:name]}" if options[:name]
244
+ uri = @api_endpoint.merge("#{@api_endpoint.path}/#{@api_version}/#{path}")
245
+
246
+ params = {}
247
+ WATCH_ARGUMENTS.each { |k, v| params[k] = options[v] if options[v] }
248
+ uri.query = URI.encode_www_form(params) if params.any?
249
+
250
+ Kubeclient::Common::WatchStream.new(uri, http_options(uri))
251
+ end
252
+
253
+ # Accepts the following string options:
254
+ # :namespace - the namespace of the entity.
255
+ # :label_selector - a selector to restrict the list of returned objects by their labels.
256
+ # :field_selector - a selector to restrict the list of returned objects by their fields.
257
+ def get_entities(entity_type, klass, resource_name, options = {})
258
+ params = {}
259
+ SEARCH_ARGUMENTS.each { |k, v| params[k] = options[v] if options[v] }
260
+
261
+ ns_prefix = build_namespace_prefix(options[:namespace])
262
+
263
+ response = handle_exception do
264
+ rest_client[ns_prefix + resource_name]
265
+ .get({ 'params' => params }.merge(@headers))
266
+ end
267
+
268
+ result = JSON.parse(response)
269
+
270
+ resource_version =
271
+ result.fetch('resourceVersion') do
272
+ result.fetch('metadata', {}).fetch('resourceVersion', nil)
273
+ end
274
+
275
+ # result['items'] might be nil due to https://github.com/kubernetes/kubernetes/issues/13096
276
+ collection = result['items'].to_a.map { |item| new_entity(item, klass) }
277
+
278
+ Kubeclient::Common::EntityList.new(entity_type, resource_version, collection)
279
+ end
280
+
281
+ def get_entity(klass, resource_name, name, namespace = nil)
282
+ ns_prefix = build_namespace_prefix(namespace)
283
+ response = handle_exception do
284
+ rest_client[ns_prefix + resource_name + "/#{name}"]
285
+ .get(@headers)
286
+ end
287
+ result = JSON.parse(response)
288
+ new_entity(result, klass)
289
+ end
290
+
291
+ def delete_entity(resource_name, name, namespace = nil)
292
+ ns_prefix = build_namespace_prefix(namespace)
293
+ handle_exception do
294
+ rest_client[ns_prefix + resource_name + "/#{name}"]
295
+ .delete(@headers)
296
+ end
297
+ end
298
+
299
+ def create_entity(entity_type, resource_name, entity_config, klass)
300
+ # Duplicate the entity_config to a hash so that when we assign
301
+ # kind and apiVersion, this does not mutate original entity_config obj.
302
+ hash = entity_config.to_hash
303
+
304
+ ns_prefix = build_namespace_prefix(hash[:metadata][:namespace])
305
+
306
+ # TODO: temporary solution to add "kind" and apiVersion to request
307
+ # until this issue is solved
308
+ # https://github.com/GoogleCloudPlatform/kubernetes/issues/6439
309
+ # TODO: #2 solution for
310
+ # https://github.com/kubernetes/kubernetes/issues/8115
311
+ hash[:kind] = (entity_type.eql?('Endpoint') ? 'Endpoints' : entity_type)
312
+ hash[:apiVersion] = @api_group + @api_version
313
+ response = handle_exception do
314
+ rest_client[ns_prefix + resource_name]
315
+ .post(hash.to_json, { 'Content-Type' => 'application/json' }.merge(@headers))
316
+ end
317
+ result = JSON.parse(response)
318
+ new_entity(result, klass)
319
+ end
320
+
321
+ def update_entity(resource_name, entity_config)
322
+ name = entity_config[:metadata][:name]
323
+ ns_prefix = build_namespace_prefix(entity_config[:metadata][:namespace])
324
+ handle_exception do
325
+ rest_client[ns_prefix + resource_name + "/#{name}"]
326
+ .put(entity_config.to_h.to_json, { 'Content-Type' => 'application/json' }.merge(@headers))
327
+ end
328
+ end
329
+
330
+ def patch_entity(resource_name, name, patch, namespace = nil)
331
+ ns_prefix = build_namespace_prefix(namespace)
332
+
333
+ handle_exception do
334
+ rest_client[ns_prefix + resource_name + "/#{name}"]
335
+ .patch(
336
+ patch.to_json,
337
+ { 'Content-Type' => 'application/strategic-merge-patch+json' }.merge(@headers)
338
+ )
339
+ end
340
+ end
341
+
342
+ def rollback_entity(resource_name, name, entity_config: {}, namespace: nil)
343
+ ns_prefix = build_namespace_prefix(namespace)
344
+
345
+ hash = entity_config.to_hash
346
+ kind = resource_name.eql?('deployments') ? 'deployment' : resource_name
347
+ hash[:kind] = "#{kind.capitalize}Rollback"
348
+ hash[:apiVersion] = "extensions/v1beta1"
349
+ hash[:rollbackTo] ||= {}
350
+ hash[:name] ||= name
351
+
352
+ handle_exception do
353
+ rest_client[ns_prefix + resource_name + "/#{name}/rollback"]
354
+ .post(
355
+ hash.to_json,
356
+ { 'Content-Type' => 'application/json' }.merge(@headers)
357
+ )
358
+ end
359
+ end
360
+
361
+ def new_entity(hash, klass)
362
+ klass.new(hash)
363
+ end
364
+
365
+ def all_entities
366
+ discover unless @discovered
367
+ @entities.values.each_with_object({}) do |entity, result_hash|
368
+ # method call for get each entities
369
+ # build hash of entity name to array of the entities
370
+ method_name = "get_#{entity.method_names[1]}"
371
+ begin
372
+ result_hash[entity.method_names[0]] = send(method_name)
373
+ rescue KubeException
374
+ next # do not fail due to resources not supporting get
375
+ end
376
+ end
377
+ end
378
+
379
+ def get_pod_log(pod_name, namespace, container: nil, previous: false)
380
+ params = {}
381
+ params[:previous] = true if previous
382
+ params[:container] = container if container
383
+
384
+ ns = build_namespace_prefix(namespace)
385
+ handle_exception do
386
+ rest_client[ns + "pods/#{pod_name}/log"]
387
+ .get({ 'params' => params }.merge(@headers))
388
+ end
389
+ end
390
+
391
+ def watch_pod_log(pod_name, namespace, container: nil)
392
+ # Adding the "follow=true" query param tells the Kubernetes API to keep
393
+ # the connection open and stream updates to the log.
394
+ params = { follow: true }
395
+ params[:container] = container if container
396
+
397
+ ns = build_namespace_prefix(namespace)
398
+
399
+ uri = @api_endpoint.dup
400
+ uri.path += "/#{@api_version}/#{ns}pods/#{pod_name}/log"
401
+ uri.query = URI.encode_www_form(params)
402
+
403
+ Kubeclient::Common::WatchStream.new(uri, http_options(uri), format: :text)
404
+ end
405
+
406
+ def proxy_url(kind, name, port, namespace = '')
407
+ discover unless @discovered
408
+ entity_name_plural =
409
+ if %w(services pods nodes).include?(kind.to_s)
410
+ kind.to_s
411
+ else
412
+ @entities[kind.to_s].resource_name
413
+ end
414
+ ns_prefix = build_namespace_prefix(namespace)
415
+ # TODO: Change this once services supports the new scheme
416
+ if entity_name_plural == 'pods'
417
+ rest_client["#{ns_prefix}#{entity_name_plural}/#{name}:#{port}/proxy"].url
418
+ else
419
+ rest_client["proxy/#{ns_prefix}#{entity_name_plural}/#{name}:#{port}"].url
420
+ end
421
+ end
422
+
423
+ def process_template(template)
424
+ ns_prefix = build_namespace_prefix(template[:metadata][:namespace])
425
+ response = handle_exception do
426
+ rest_client[ns_prefix + 'processedtemplates']
427
+ .post(template.to_h.to_json, { 'Content-Type' => 'application/json' }.merge(@headers))
428
+ end
429
+ JSON.parse(response)
430
+ end
431
+
432
+ def api_valid?
433
+ result = api
434
+ result.is_a?(Hash) && (result['versions'] || []).any? do |group|
435
+ @api_group.empty? ? group.include?(@api_version) : group['version'] == @api_version
436
+ end
437
+ end
438
+
439
+ def api
440
+ response = handle_exception { create_rest_client.get(@headers) }
441
+ JSON.parse(response)
442
+ end
443
+
444
+ private
445
+
446
+ def load_entities
447
+ @entities = {}
448
+ fetch_entities['resources'].each do |resource|
449
+ next if resource['name'].include?('/')
450
+ resource['kind'] ||=
451
+ Kubeclient::Common::MissingKindCompatibility.resource_kind(resource['name'])
452
+ entity = ClientMixin.parse_definition(resource['kind'], resource['name'])
453
+ @entities[entity.method_names[0]] = entity if entity
454
+ end
455
+ end
456
+
457
+ def fetch_entities
458
+ JSON.parse(handle_exception { rest_client.get(@headers) })
459
+ end
460
+
461
+ def bearer_token(bearer_token)
462
+ @headers ||= {}
463
+ @headers[:Authorization] = "Bearer #{bearer_token}"
464
+ end
465
+
466
+ def validate_auth_options(opts)
467
+ # maintain backward compatibility:
468
+ opts[:username] = opts[:user] if opts[:user]
469
+
470
+ if [:bearer_token, :bearer_token_file, :username].count { |key| opts[key] } > 1
471
+ raise(
472
+ ArgumentError,
473
+ 'Invalid auth options: specify only one of username/password,' \
474
+ ' bearer_token or bearer_token_file'
475
+ )
476
+ elsif [:username, :password].count { |key| opts[key] } == 1
477
+ raise ArgumentError, 'Basic auth requires both username & password'
478
+ end
479
+ end
480
+
481
+ def validate_bearer_token_file
482
+ msg = "Token file #{@auth_options[:bearer_token_file]} does not exist"
483
+ raise ArgumentError, msg unless File.file?(@auth_options[:bearer_token_file])
484
+
485
+ msg = "Cannot read token file #{@auth_options[:bearer_token_file]}"
486
+ raise ArgumentError, msg unless File.readable?(@auth_options[:bearer_token_file])
487
+ end
488
+
489
+ def http_options(uri)
490
+ options = {
491
+ basic_auth_user: @auth_options[:username],
492
+ basic_auth_password: @auth_options[:password],
493
+ headers: @headers,
494
+ http_proxy_uri: @http_proxy_uri
495
+ }
496
+
497
+ if uri.scheme == 'https'
498
+ options[:ssl] = {
499
+ ca_file: @ssl_options[:ca_file],
500
+ cert: @ssl_options[:client_cert],
501
+ cert_store: @ssl_options[:cert_store],
502
+ key: @ssl_options[:client_key],
503
+ # ruby HTTP uses verify_mode instead of verify_ssl
504
+ # http://ruby-doc.org/stdlib-1.9.3/libdoc/openssl/rdoc/OpenSSL/SSL/SSLContext.html
505
+ verify_mode: @ssl_options[:verify_ssl]
506
+ }
507
+ end
508
+
509
+ options.merge(@socket_options)
510
+ end
511
+ end
512
+ end
@@ -0,0 +1,126 @@
1
+ require 'yaml'
2
+ require 'base64'
3
+ require 'pathname'
4
+
5
+ module Kubeclient
6
+ # Kubernetes client configuration class
7
+ class Config
8
+ # Kubernetes client configuration context class
9
+ class Context
10
+ attr_reader :api_endpoint, :api_version, :ssl_options, :auth_options
11
+
12
+ def initialize(api_endpoint, api_version, ssl_options, auth_options)
13
+ @api_endpoint = api_endpoint
14
+ @api_version = api_version
15
+ @ssl_options = ssl_options
16
+ @auth_options = auth_options
17
+ end
18
+ end
19
+
20
+ def initialize(kcfg, kcfg_path)
21
+ @kcfg = kcfg
22
+ @kcfg_path = kcfg_path
23
+ raise 'Unknown kubeconfig version' if @kcfg['apiVersion'] != 'v1'
24
+ end
25
+
26
+ def self.read(filename)
27
+ Config.new(YAML.load_file(filename), File.dirname(filename))
28
+ end
29
+
30
+ def contexts
31
+ @kcfg['contexts'].map { |x| x['name'] }
32
+ end
33
+
34
+ def context(context_name = nil)
35
+ cluster, user = fetch_context(context_name || @kcfg['current-context'])
36
+
37
+ ca_cert_data = fetch_cluster_ca_data(cluster)
38
+ client_cert_data = fetch_user_cert_data(user)
39
+ client_key_data = fetch_user_key_data(user)
40
+ auth_options = fetch_user_auth_options(user)
41
+
42
+ ssl_options = {}
43
+
44
+ if !ca_cert_data.nil?
45
+ cert_store = OpenSSL::X509::Store.new
46
+ cert_store.add_cert(OpenSSL::X509::Certificate.new(ca_cert_data))
47
+ ssl_options[:verify_ssl] = OpenSSL::SSL::VERIFY_PEER
48
+ ssl_options[:cert_store] = cert_store
49
+ else
50
+ ssl_options[:verify_ssl] = OpenSSL::SSL::VERIFY_NONE
51
+ end
52
+
53
+ unless client_cert_data.nil?
54
+ ssl_options[:client_cert] = OpenSSL::X509::Certificate.new(client_cert_data)
55
+ end
56
+
57
+ unless client_key_data.nil?
58
+ ssl_options[:client_key] = OpenSSL::PKey.read(client_key_data)
59
+ end
60
+
61
+ Context.new(cluster['server'], @kcfg['apiVersion'], ssl_options, auth_options)
62
+ end
63
+
64
+ private
65
+
66
+ def ext_file_path(path)
67
+ Pathname(path).absolute? ? path : File.join(@kcfg_path, path)
68
+ end
69
+
70
+ def fetch_context(context_name)
71
+ context = @kcfg['contexts'].detect do |x|
72
+ break x['context'] if x['name'] == context_name
73
+ end
74
+
75
+ raise "Unknown context #{context_name}" unless context
76
+
77
+ cluster = @kcfg['clusters'].detect do |x|
78
+ break x['cluster'] if x['name'] == context['cluster']
79
+ end
80
+
81
+ raise "Unknown cluster #{context['cluster']}" unless cluster
82
+
83
+ user = @kcfg['users'].detect do |x|
84
+ break x['user'] if x['name'] == context['user']
85
+ end || {}
86
+
87
+ [cluster, user]
88
+ end
89
+
90
+ def fetch_cluster_ca_data(cluster)
91
+ if cluster.key?('certificate-authority')
92
+ File.read(ext_file_path(cluster['certificate-authority']))
93
+ elsif cluster.key?('certificate-authority-data')
94
+ Base64.decode64(cluster['certificate-authority-data'])
95
+ end
96
+ end
97
+
98
+ def fetch_user_cert_data(user)
99
+ if user.key?('client-certificate')
100
+ File.read(ext_file_path(user['client-certificate']))
101
+ elsif user.key?('client-certificate-data')
102
+ Base64.decode64(user['client-certificate-data'])
103
+ end
104
+ end
105
+
106
+ def fetch_user_key_data(user)
107
+ if user.key?('client-key')
108
+ File.read(ext_file_path(user['client-key']))
109
+ elsif user.key?('client-key-data')
110
+ Base64.decode64(user['client-key-data'])
111
+ end
112
+ end
113
+
114
+ def fetch_user_auth_options(user)
115
+ options = {}
116
+ if user.key?('token')
117
+ options[:bearer_token] = user['token']
118
+ else
119
+ %w(username password).each do |attr|
120
+ options[attr.to_sym] = user[attr] if user.key?(attr)
121
+ end
122
+ end
123
+ options
124
+ end
125
+ end
126
+ end
@@ -0,0 +1,16 @@
1
+ require 'delegate'
2
+ module Kubeclient
3
+ module Common
4
+ # Kubernetes Entity List
5
+ class EntityList < DelegateClass(Array)
6
+ attr_reader :kind, :resourceVersion
7
+
8
+ def initialize(kind, resource_version, list)
9
+ @kind = kind
10
+ # rubocop:disable Style/VariableName
11
+ @resourceVersion = resource_version
12
+ super(list)
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,14 @@
1
+ # Kubernetes HTTP Exceptions
2
+ class KubeException < StandardError
3
+ attr_reader :error_code, :message, :response
4
+
5
+ def initialize(error_code, message, response)
6
+ @error_code = error_code
7
+ @message = message
8
+ @response = response
9
+ end
10
+
11
+ def to_s
12
+ 'HTTP status code ' + @error_code.to_s + ', ' + @message
13
+ end
14
+ end