ruboty-k8s 0.1.0 → 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (98) hide show
  1. checksums.yaml +4 -4
  2. data/lib/kubeclient/.gitignore +15 -0
  3. data/lib/kubeclient/.rubocop.yml +16 -0
  4. data/lib/kubeclient/.travis.yml +12 -0
  5. data/lib/kubeclient/Gemfile +4 -0
  6. data/lib/kubeclient/LICENSE.txt +22 -0
  7. data/lib/kubeclient/README.md +428 -0
  8. data/lib/kubeclient/Rakefile +12 -0
  9. data/lib/kubeclient/kubeclient.gemspec +31 -0
  10. data/lib/kubeclient/lib/kubeclient/common.rb +512 -0
  11. data/lib/kubeclient/lib/kubeclient/config.rb +126 -0
  12. data/lib/kubeclient/lib/kubeclient/entity_list.rb +16 -0
  13. data/lib/kubeclient/lib/kubeclient/kube_exception.rb +14 -0
  14. data/lib/kubeclient/lib/kubeclient/missing_kind_compatibility.rb +68 -0
  15. data/lib/kubeclient/lib/kubeclient/version.rb +4 -0
  16. data/lib/kubeclient/lib/kubeclient/watch_notice.rb +7 -0
  17. data/lib/kubeclient/lib/kubeclient/watch_stream.rb +80 -0
  18. data/lib/kubeclient/lib/kubeclient.rb +32 -0
  19. data/lib/kubeclient/test/cassettes/kubernetes_guestbook.yml +879 -0
  20. data/lib/kubeclient/test/config/allinone.kubeconfig +20 -0
  21. data/lib/kubeclient/test/config/external-ca.pem +18 -0
  22. data/lib/kubeclient/test/config/external-cert.pem +19 -0
  23. data/lib/kubeclient/test/config/external-key.rsa +27 -0
  24. data/lib/kubeclient/test/config/external.kubeconfig +20 -0
  25. data/lib/kubeclient/test/config/nouser.kubeconfig +16 -0
  26. data/lib/kubeclient/test/config/userauth.kubeconfig +28 -0
  27. data/lib/kubeclient/test/json/bindings_list.json +10 -0
  28. data/lib/kubeclient/test/json/component_status.json +17 -0
  29. data/lib/kubeclient/test/json/component_status_list.json +52 -0
  30. data/lib/kubeclient/test/json/config_map_list.json +9 -0
  31. data/lib/kubeclient/test/json/core_api_resource_list.json +181 -0
  32. data/lib/kubeclient/test/json/core_api_resource_list_without_kind.json +129 -0
  33. data/lib/kubeclient/test/json/core_oapi_resource_list_without_kind.json +197 -0
  34. data/lib/kubeclient/test/json/created_endpoint.json +28 -0
  35. data/lib/kubeclient/test/json/created_namespace.json +20 -0
  36. data/lib/kubeclient/test/json/created_secret.json +16 -0
  37. data/lib/kubeclient/test/json/created_service.json +31 -0
  38. data/lib/kubeclient/test/json/empty_pod_list.json +9 -0
  39. data/lib/kubeclient/test/json/endpoint_list.json +48 -0
  40. data/lib/kubeclient/test/json/entity_list.json +56 -0
  41. data/lib/kubeclient/test/json/event_list.json +35 -0
  42. data/lib/kubeclient/test/json/limit_range.json +23 -0
  43. data/lib/kubeclient/test/json/limit_range_list.json +31 -0
  44. data/lib/kubeclient/test/json/namespace.json +13 -0
  45. data/lib/kubeclient/test/json/namespace_exception.json +8 -0
  46. data/lib/kubeclient/test/json/namespace_list.json +32 -0
  47. data/lib/kubeclient/test/json/node.json +29 -0
  48. data/lib/kubeclient/test/json/node_list.json +37 -0
  49. data/lib/kubeclient/test/json/persistent_volume.json +37 -0
  50. data/lib/kubeclient/test/json/persistent_volume_claim.json +32 -0
  51. data/lib/kubeclient/test/json/persistent_volume_claim_list.json +40 -0
  52. data/lib/kubeclient/test/json/persistent_volume_claims_nil_items.json +8 -0
  53. data/lib/kubeclient/test/json/persistent_volume_list.json +45 -0
  54. data/lib/kubeclient/test/json/pod.json +92 -0
  55. data/lib/kubeclient/test/json/pod_list.json +79 -0
  56. data/lib/kubeclient/test/json/pod_template_list.json +9 -0
  57. data/lib/kubeclient/test/json/processed_template.json +27 -0
  58. data/lib/kubeclient/test/json/replication_controller.json +57 -0
  59. data/lib/kubeclient/test/json/replication_controller_list.json +66 -0
  60. data/lib/kubeclient/test/json/resource_quota.json +46 -0
  61. data/lib/kubeclient/test/json/resource_quota_list.json +54 -0
  62. data/lib/kubeclient/test/json/secret_list.json +44 -0
  63. data/lib/kubeclient/test/json/service.json +33 -0
  64. data/lib/kubeclient/test/json/service_account.json +25 -0
  65. data/lib/kubeclient/test/json/service_account_list.json +82 -0
  66. data/lib/kubeclient/test/json/service_illegal_json_404.json +1 -0
  67. data/lib/kubeclient/test/json/service_list.json +97 -0
  68. data/lib/kubeclient/test/json/service_patch.json +25 -0
  69. data/lib/kubeclient/test/json/service_update.json +22 -0
  70. data/lib/kubeclient/test/json/versions_list.json +6 -0
  71. data/lib/kubeclient/test/json/watch_stream.json +3 -0
  72. data/lib/kubeclient/test/test_common.rb +32 -0
  73. data/lib/kubeclient/test/test_component_status.rb +30 -0
  74. data/lib/kubeclient/test/test_config.rb +72 -0
  75. data/lib/kubeclient/test/test_endpoint.rb +35 -0
  76. data/lib/kubeclient/test/test_guestbook_go.rb +238 -0
  77. data/lib/kubeclient/test/test_helper.rb +10 -0
  78. data/lib/kubeclient/test/test_kubeclient.rb +611 -0
  79. data/lib/kubeclient/test/test_limit_range.rb +27 -0
  80. data/lib/kubeclient/test/test_missing_methods.rb +42 -0
  81. data/lib/kubeclient/test/test_namespace.rb +61 -0
  82. data/lib/kubeclient/test/test_node.rb +33 -0
  83. data/lib/kubeclient/test/test_persistent_volume.rb +30 -0
  84. data/lib/kubeclient/test/test_persistent_volume_claim.rb +30 -0
  85. data/lib/kubeclient/test/test_pod.rb +29 -0
  86. data/lib/kubeclient/test/test_pod_log.rb +50 -0
  87. data/lib/kubeclient/test/test_process_template.rb +44 -0
  88. data/lib/kubeclient/test/test_replication_controller.rb +27 -0
  89. data/lib/kubeclient/test/test_resource_list_without_kind.rb +78 -0
  90. data/lib/kubeclient/test/test_resource_quota.rb +25 -0
  91. data/lib/kubeclient/test/test_secret.rb +70 -0
  92. data/lib/kubeclient/test/test_service.rb +293 -0
  93. data/lib/kubeclient/test/test_service_account.rb +28 -0
  94. data/lib/kubeclient/test/test_watch.rb +119 -0
  95. data/lib/kubeclient/test/txt/pod_log.txt +6 -0
  96. data/lib/kubeclient/test/valid_token_file +1 -0
  97. data/lib/ruboty/k8s/version.rb +1 -1
  98. metadata +96 -1
@@ -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