kubeclient 4.7.0 → 4.12.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 (117) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/actions.yml +43 -0
  3. data/.rubocop.yml +111 -14
  4. data/CHANGELOG.md +119 -0
  5. data/README.md +41 -4
  6. data/RELEASING.md +8 -8
  7. data/kubeclient.gemspec +11 -7
  8. data/lib/kubeclient/aws_eks_credentials.rb +17 -8
  9. data/lib/kubeclient/common.rb +55 -21
  10. data/lib/kubeclient/config.rb +33 -13
  11. data/lib/kubeclient/exec_credentials.rb +33 -4
  12. data/lib/kubeclient/version.rb +1 -1
  13. data/lib/kubeclient/watch_stream.rb +1 -0
  14. metadata +46 -222
  15. data/.travis.yml +0 -29
  16. data/test/cassettes/kubernetes_guestbook.yml +0 -879
  17. data/test/config/allinone.kubeconfig +0 -20
  18. data/test/config/execauth.kubeconfig +0 -62
  19. data/test/config/external-ca.pem +0 -18
  20. data/test/config/external-cert.pem +0 -19
  21. data/test/config/external-key.rsa +0 -27
  22. data/test/config/external.kubeconfig +0 -20
  23. data/test/config/gcpauth.kubeconfig +0 -22
  24. data/test/config/gcpcmdauth.kubeconfig +0 -26
  25. data/test/config/nouser.kubeconfig +0 -16
  26. data/test/config/oidcauth.kubeconfig +0 -25
  27. data/test/config/timestamps.kubeconfig +0 -25
  28. data/test/config/userauth.kubeconfig +0 -28
  29. data/test/json/bindings_list.json +0 -10
  30. data/test/json/component_status.json +0 -17
  31. data/test/json/component_status_list.json +0 -52
  32. data/test/json/config.istio.io_api_resource_list.json +0 -679
  33. data/test/json/config_map_list.json +0 -9
  34. data/test/json/core_api_resource_list.json +0 -181
  35. data/test/json/core_api_resource_list_without_kind.json +0 -129
  36. data/test/json/core_oapi_resource_list_without_kind.json +0 -197
  37. data/test/json/created_endpoint.json +0 -28
  38. data/test/json/created_namespace.json +0 -20
  39. data/test/json/created_secret.json +0 -16
  40. data/test/json/created_security_context_constraint.json +0 -65
  41. data/test/json/created_service.json +0 -31
  42. data/test/json/empty_pod_list.json +0 -9
  43. data/test/json/endpoint_list.json +0 -48
  44. data/test/json/entity_list.json +0 -56
  45. data/test/json/event_list.json +0 -35
  46. data/test/json/extensions_v1beta1_api_resource_list.json +0 -217
  47. data/test/json/limit_range.json +0 -23
  48. data/test/json/limit_range_list.json +0 -31
  49. data/test/json/namespace.json +0 -13
  50. data/test/json/namespace_exception.json +0 -8
  51. data/test/json/namespace_list.json +0 -32
  52. data/test/json/node.json +0 -29
  53. data/test/json/node_list.json +0 -37
  54. data/test/json/node_notice.json +0 -160
  55. data/test/json/persistent_volume.json +0 -37
  56. data/test/json/persistent_volume_claim.json +0 -32
  57. data/test/json/persistent_volume_claim_list.json +0 -40
  58. data/test/json/persistent_volume_claims_nil_items.json +0 -8
  59. data/test/json/persistent_volume_list.json +0 -45
  60. data/test/json/pod.json +0 -92
  61. data/test/json/pod_list.json +0 -79
  62. data/test/json/pod_template_list.json +0 -9
  63. data/test/json/pods_1.json +0 -265
  64. data/test/json/pods_2.json +0 -102
  65. data/test/json/pods_410.json +0 -9
  66. data/test/json/processed_template.json +0 -27
  67. data/test/json/replication_controller.json +0 -57
  68. data/test/json/replication_controller_list.json +0 -66
  69. data/test/json/resource_quota.json +0 -46
  70. data/test/json/resource_quota_list.json +0 -54
  71. data/test/json/secret_list.json +0 -44
  72. data/test/json/security.openshift.io_api_resource_list.json +0 -69
  73. data/test/json/security_context_constraint_list.json +0 -375
  74. data/test/json/service.json +0 -33
  75. data/test/json/service_account.json +0 -25
  76. data/test/json/service_account_list.json +0 -82
  77. data/test/json/service_illegal_json_404.json +0 -1
  78. data/test/json/service_json_patch.json +0 -26
  79. data/test/json/service_list.json +0 -97
  80. data/test/json/service_merge_patch.json +0 -26
  81. data/test/json/service_patch.json +0 -25
  82. data/test/json/service_update.json +0 -22
  83. data/test/json/template.json +0 -27
  84. data/test/json/template.openshift.io_api_resource_list.json +0 -75
  85. data/test/json/template_list.json +0 -35
  86. data/test/json/versions_list.json +0 -6
  87. data/test/json/watch_stream.json +0 -3
  88. data/test/test_common.rb +0 -95
  89. data/test/test_component_status.rb +0 -29
  90. data/test/test_config.rb +0 -222
  91. data/test/test_endpoint.rb +0 -54
  92. data/test/test_exec_credentials.rb +0 -125
  93. data/test/test_gcp_command_credentials.rb +0 -27
  94. data/test/test_google_application_default_credentials.rb +0 -15
  95. data/test/test_guestbook_go.rb +0 -235
  96. data/test/test_helper.rb +0 -18
  97. data/test/test_kubeclient.rb +0 -881
  98. data/test/test_limit_range.rb +0 -25
  99. data/test/test_missing_methods.rb +0 -80
  100. data/test/test_namespace.rb +0 -59
  101. data/test/test_node.rb +0 -70
  102. data/test/test_oidc_auth_provider.rb +0 -103
  103. data/test/test_persistent_volume.rb +0 -29
  104. data/test/test_persistent_volume_claim.rb +0 -28
  105. data/test/test_pod.rb +0 -81
  106. data/test/test_pod_log.rb +0 -157
  107. data/test/test_process_template.rb +0 -80
  108. data/test/test_replication_controller.rb +0 -47
  109. data/test/test_resource_list_without_kind.rb +0 -78
  110. data/test/test_resource_quota.rb +0 -23
  111. data/test/test_secret.rb +0 -62
  112. data/test/test_security_context_constraint.rb +0 -62
  113. data/test/test_service.rb +0 -330
  114. data/test/test_service_account.rb +0 -26
  115. data/test/test_watch.rb +0 -195
  116. data/test/txt/pod_log.txt +0 -6
  117. data/test/valid_token_file +0 -1
@@ -5,7 +5,7 @@ module Kubeclient
5
5
  # Common methods
6
6
  # this is mixed in by other gems
7
7
  module ClientMixin
8
- ENTITY_METHODS = %w[get watch delete create update patch json_patch merge_patch].freeze
8
+ ENTITY_METHODS = %w[get watch delete create update patch json_patch merge_patch apply].freeze
9
9
 
10
10
  DEFAULT_SSL_OPTIONS = {
11
11
  client_cert: nil,
@@ -78,7 +78,7 @@ module Kubeclient
78
78
  @api_version = version
79
79
  @headers = {}
80
80
  @ssl_options = ssl_options
81
- @auth_options = auth_options
81
+ @auth_options = auth_options.dup
82
82
  @socket_options = socket_options
83
83
  # Allow passing partial timeouts hash, without unspecified
84
84
  # @timeouts[:foo] == nil resulting in infinite timeout.
@@ -87,11 +87,11 @@ module Kubeclient
87
87
  @http_max_redirects = http_max_redirects
88
88
  @as = as
89
89
 
90
- if auth_options[:bearer_token]
91
- bearer_token(@auth_options[:bearer_token])
92
- elsif auth_options[:bearer_token_file]
90
+ if auth_options[:bearer_token_file]
93
91
  validate_bearer_token_file
94
92
  bearer_token(File.read(@auth_options[:bearer_token_file]))
93
+ elsif auth_options[:bearer_token]
94
+ bearer_token(@auth_options[:bearer_token])
95
95
  end
96
96
  end
97
97
 
@@ -136,6 +136,11 @@ module Kubeclient
136
136
  @discovered = true
137
137
  end
138
138
 
139
+ def get_headers
140
+ bearer_token(File.read(@auth_options[:bearer_token_file])) if @auth_options[:bearer_token_file]
141
+ @headers
142
+ end
143
+
139
144
  def self.parse_definition(kind, name)
140
145
  # Kubernetes gives us 3 inputs:
141
146
  # kind: "ComponentStatus", "NetworkPolicy", "Endpoints"
@@ -194,10 +199,22 @@ module Kubeclient
194
199
  def handle_uri(uri, path)
195
200
  raise ArgumentError, 'Missing uri' unless uri
196
201
  @api_endpoint = (uri.is_a?(URI) ? uri : URI.parse(uri))
197
- @api_endpoint.path = path if @api_endpoint.path.empty?
198
- @api_endpoint.path = @api_endpoint.path.chop if @api_endpoint.path.end_with?('/')
199
- components = @api_endpoint.path.to_s.split('/') # ["", "api"] or ["", "apis", batch]
200
- @api_group = components.length > 2 ? components[2] + '/' : ''
202
+
203
+ # This regex will anchor at the last `/api`, `/oapi` or`/apis/:group`) part of the URL
204
+ # The whole path will be matched and if existing, the api_group will be extracted.
205
+ re = /^(?<path>.*\/o?api(?:s\/(?<apigroup>[^\/]+))?)$/mi
206
+ match = re.match(@api_endpoint.path.chomp('/'))
207
+
208
+ if match
209
+ # Since `re` captures 2 groups, match will always have 3 elements
210
+ # If thus we have a non-nil value in match 2, this is our api_group.
211
+ @api_group = match[:apigroup].nil? ? '' : match[:apigroup] + '/'
212
+ @api_endpoint.path = match[:path]
213
+ else
214
+ # This is a fallback, for when `/api` was not provided as part of the uri
215
+ @api_group = ''
216
+ @api_endpoint.path = @api_endpoint.path.chomp('/') + path
217
+ end
201
218
  end
202
219
 
203
220
  def build_namespace_prefix(namespace)
@@ -254,6 +271,10 @@ module Kubeclient
254
271
  do |name, patch, namespace = nil|
255
272
  patch_entity(entity.resource_name, name, patch, 'merge-patch', namespace)
256
273
  end
274
+
275
+ define_singleton_method("apply_#{entity.method_names[0]}") do |resource, opts = {}|
276
+ apply_entity(entity.resource_name, resource, **opts)
277
+ end
257
278
  end
258
279
  end
259
280
  # rubocop:enable Metrics/BlockLength
@@ -333,7 +354,7 @@ module Kubeclient
333
354
  ns_prefix = build_namespace_prefix(options[:namespace])
334
355
  response = handle_exception do
335
356
  rest_client[ns_prefix + resource_name]
336
- .get({ 'params' => params }.merge(@headers))
357
+ .get({ 'params' => params }.merge(get_headers))
337
358
  end
338
359
  format_response(options[:as] || @as, response.body, entity_type)
339
360
  end
@@ -346,7 +367,7 @@ module Kubeclient
346
367
  ns_prefix = build_namespace_prefix(namespace)
347
368
  response = handle_exception do
348
369
  rest_client[ns_prefix + resource_name + "/#{name}"]
349
- .get(@headers)
370
+ .get(get_headers)
350
371
  end
351
372
  format_response(options[:as] || @as, response.body)
352
373
  end
@@ -362,7 +383,7 @@ module Kubeclient
362
383
  rs.options.merge(
363
384
  method: :delete,
364
385
  url: rs.url,
365
- headers: { 'Content-Type' => 'application/json' }.merge(@headers),
386
+ headers: { 'Content-Type' => 'application/json' }.merge(get_headers),
366
387
  payload: payload
367
388
  )
368
389
  )
@@ -384,7 +405,7 @@ module Kubeclient
384
405
  hash[:apiVersion] = @api_group + @api_version
385
406
  response = handle_exception do
386
407
  rest_client[ns_prefix + resource_name]
387
- .post(hash.to_json, { 'Content-Type' => 'application/json' }.merge(@headers))
408
+ .post(hash.to_json, { 'Content-Type' => 'application/json' }.merge(get_headers))
388
409
  end
389
410
  format_response(@as, response.body)
390
411
  end
@@ -394,7 +415,7 @@ module Kubeclient
394
415
  ns_prefix = build_namespace_prefix(entity_config[:metadata][:namespace])
395
416
  response = handle_exception do
396
417
  rest_client[ns_prefix + resource_name + "/#{name}"]
397
- .put(entity_config.to_h.to_json, { 'Content-Type' => 'application/json' }.merge(@headers))
418
+ .put(entity_config.to_h.to_json, { 'Content-Type' => 'application/json' }.merge(get_headers))
398
419
  end
399
420
  format_response(@as, response.body)
400
421
  end
@@ -405,7 +426,20 @@ module Kubeclient
405
426
  rest_client[ns_prefix + resource_name + "/#{name}"]
406
427
  .patch(
407
428
  patch.to_json,
408
- { 'Content-Type' => "application/#{strategy}+json" }.merge(@headers)
429
+ { 'Content-Type' => "application/#{strategy}+json" }.merge(get_headers)
430
+ )
431
+ end
432
+ format_response(@as, response.body)
433
+ end
434
+
435
+ def apply_entity(resource_name, resource, field_manager:, force: true)
436
+ name = "#{resource[:metadata][:name]}?fieldManager=#{field_manager}&force=#{force}"
437
+ ns_prefix = build_namespace_prefix(resource[:metadata][:namespace])
438
+ response = handle_exception do
439
+ rest_client[ns_prefix + resource_name + "/#{name}"]
440
+ .patch(
441
+ resource.to_json,
442
+ { 'Content-Type' => 'application/apply-patch+yaml' }.merge(get_headers)
409
443
  )
410
444
  end
411
445
  format_response(@as, response.body)
@@ -439,7 +473,7 @@ module Kubeclient
439
473
  ns = build_namespace_prefix(namespace)
440
474
  handle_exception do
441
475
  rest_client[ns + "pods/#{pod_name}/log"]
442
- .get({ 'params' => params }.merge(@headers))
476
+ .get({ 'params' => params }.merge(get_headers))
443
477
  end
444
478
  end
445
479
 
@@ -477,7 +511,7 @@ module Kubeclient
477
511
  ns_prefix = build_namespace_prefix(template[:metadata][:namespace])
478
512
  response = handle_exception do
479
513
  rest_client[ns_prefix + 'processedtemplates']
480
- .post(template.to_h.to_json, { 'Content-Type' => 'application/json' }.merge(@headers))
514
+ .post(template.to_h.to_json, { 'Content-Type' => 'application/json' }.merge(get_headers))
481
515
  end
482
516
  JSON.parse(response)
483
517
  end
@@ -490,7 +524,7 @@ module Kubeclient
490
524
  end
491
525
 
492
526
  def api
493
- response = handle_exception { create_rest_client.get(@headers) }
527
+ response = handle_exception { create_rest_client.get(get_headers) }
494
528
  JSON.parse(response)
495
529
  end
496
530
 
@@ -564,7 +598,7 @@ module Kubeclient
564
598
  end
565
599
 
566
600
  def fetch_entities
567
- JSON.parse(handle_exception { rest_client.get(@headers) })
601
+ JSON.parse(handle_exception { rest_client.get(get_headers) })
568
602
  end
569
603
 
570
604
  def bearer_token(bearer_token)
@@ -609,11 +643,11 @@ module Kubeclient
609
643
  options = {
610
644
  basic_auth_user: @auth_options[:username],
611
645
  basic_auth_password: @auth_options[:password],
612
- headers: @headers,
646
+ headers: get_headers,
613
647
  http_proxy_uri: @http_proxy_uri,
614
648
  http_max_redirects: http_max_redirects
615
649
  }
616
-
650
+ options[:bearer_token_file] = @auth_options[:bearer_token_file] if @auth_options[:bearer_token_file]
617
651
  if uri.scheme == 'https'
618
652
  options[:ssl] = {
619
653
  ca_file: @ssl_options[:ca_file],
@@ -30,7 +30,12 @@ module Kubeclient
30
30
 
31
31
  # Builds Config instance by parsing given file, with lookups relative to file's directory.
32
32
  def self.read(filename)
33
- parsed = YAML.safe_load(File.read(filename), [Date, Time])
33
+ parsed =
34
+ if RUBY_VERSION >= '2.6'
35
+ YAML.safe_load(File.read(filename), permitted_classes: [Date, Time])
36
+ else
37
+ YAML.safe_load(File.read(filename), [Date, Time])
38
+ end
34
39
  Config.new(parsed, File.dirname(filename))
35
40
  end
36
41
 
@@ -41,20 +46,27 @@ module Kubeclient
41
46
  def context(context_name = nil)
42
47
  cluster, user, namespace = fetch_context(context_name || @kcfg['current-context'])
43
48
 
44
- ca_cert_data = fetch_cluster_ca_data(cluster)
49
+ if user.key?('exec')
50
+ exec_opts = expand_command_option(user['exec'], 'command')
51
+ user['exec_result'] = ExecCredentials.run(exec_opts)
52
+ end
53
+
45
54
  client_cert_data = fetch_user_cert_data(user)
46
55
  client_key_data = fetch_user_key_data(user)
47
56
  auth_options = fetch_user_auth_options(user)
48
57
 
49
58
  ssl_options = {}
50
59
 
51
- if !ca_cert_data.nil?
60
+ ssl_options[:verify_ssl] = if cluster['insecure-skip-tls-verify'] == true
61
+ OpenSSL::SSL::VERIFY_NONE
62
+ else
63
+ OpenSSL::SSL::VERIFY_PEER
64
+ end
65
+
66
+ if cluster_ca_data?(cluster)
52
67
  cert_store = OpenSSL::X509::Store.new
53
- cert_store.add_cert(OpenSSL::X509::Certificate.new(ca_cert_data))
54
- ssl_options[:verify_ssl] = OpenSSL::SSL::VERIFY_PEER
68
+ populate_cert_store_from_cluster_ca_data(cluster, cert_store)
55
69
  ssl_options[:cert_store] = cert_store
56
- else
57
- ssl_options[:verify_ssl] = OpenSSL::SSL::VERIFY_NONE
58
70
  end
59
71
 
60
72
  unless client_cert_data.nil?
@@ -121,11 +133,16 @@ module Kubeclient
121
133
  [cluster, user, namespace]
122
134
  end
123
135
 
124
- def fetch_cluster_ca_data(cluster)
136
+ def cluster_ca_data?(cluster)
137
+ cluster.key?('certificate-authority') || cluster.key?('certificate-authority-data')
138
+ end
139
+
140
+ def populate_cert_store_from_cluster_ca_data(cluster, cert_store)
125
141
  if cluster.key?('certificate-authority')
126
- File.read(ext_file_path(cluster['certificate-authority']))
142
+ cert_store.add_file(ext_file_path(cluster['certificate-authority']))
127
143
  elsif cluster.key?('certificate-authority-data')
128
- Base64.decode64(cluster['certificate-authority-data'])
144
+ ca_cert_data = Base64.decode64(cluster['certificate-authority-data'])
145
+ cert_store.add_cert(OpenSSL::X509::Certificate.new(ca_cert_data))
129
146
  end
130
147
  end
131
148
 
@@ -134,6 +151,8 @@ module Kubeclient
134
151
  File.read(ext_file_path(user['client-certificate']))
135
152
  elsif user.key?('client-certificate-data')
136
153
  Base64.decode64(user['client-certificate-data'])
154
+ elsif user.key?('exec_result') && user['exec_result'].key?('clientCertificateData')
155
+ user['exec_result']['clientCertificateData']
137
156
  end
138
157
  end
139
158
 
@@ -142,6 +161,8 @@ module Kubeclient
142
161
  File.read(ext_file_path(user['client-key']))
143
162
  elsif user.key?('client-key-data')
144
163
  Base64.decode64(user['client-key-data'])
164
+ elsif user.key?('exec_result') && user['exec_result'].key?('clientKeyData')
165
+ user['exec_result']['clientKeyData']
145
166
  end
146
167
  end
147
168
 
@@ -149,9 +170,8 @@ module Kubeclient
149
170
  options = {}
150
171
  if user.key?('token')
151
172
  options[:bearer_token] = user['token']
152
- elsif user.key?('exec')
153
- exec_opts = expand_command_option(user['exec'], 'command')
154
- options[:bearer_token] = Kubeclient::ExecCredentials.token(exec_opts)
173
+ elsif user.key?('exec_result') && user['exec_result'].key?('token')
174
+ options[:bearer_token] = user['exec_result']['token']
155
175
  elsif user.key?('auth-provider')
156
176
  options[:bearer_token] = fetch_token_from_provider(user['auth-provider'])
157
177
  else
@@ -6,7 +6,7 @@ module Kubeclient
6
6
  # Inspired by https://github.com/kubernetes/client-go/blob/master/plugin/pkg/client/auth/exec/exec.go
7
7
  class ExecCredentials
8
8
  class << self
9
- def token(opts)
9
+ def run(opts)
10
10
  require 'open3'
11
11
  require 'json'
12
12
 
@@ -25,7 +25,7 @@ module Kubeclient
25
25
 
26
26
  creds = JSON.parse(out)
27
27
  validate_credentials(opts, creds)
28
- creds['status']['token']
28
+ creds['status']
29
29
  end
30
30
 
31
31
  private
@@ -34,6 +34,36 @@ module Kubeclient
34
34
  raise KeyError, 'exec command is required' unless opts['command']
35
35
  end
36
36
 
37
+ def validate_client_credentials_status(status)
38
+ has_client_cert_data = status.key?('clientCertificateData')
39
+ has_client_key_data = status.key?('clientKeyData')
40
+
41
+ if has_client_cert_data && !has_client_key_data
42
+ raise 'exec plugin didn\'t return client key data'
43
+ end
44
+
45
+ if !has_client_cert_data && has_client_key_data
46
+ raise 'exec plugin didn\'t return client certificate data'
47
+ end
48
+
49
+ has_client_cert_data && has_client_key_data
50
+ end
51
+
52
+ def validate_credentials_status(status)
53
+ raise 'exec plugin didn\'t return a status field' if status.nil?
54
+
55
+ has_client_credentials = validate_client_credentials_status(status)
56
+ has_token = status.key?('token')
57
+
58
+ if has_client_credentials && has_token
59
+ raise 'exec plugin returned both token and client data'
60
+ end
61
+
62
+ return if has_client_credentials || has_token
63
+
64
+ raise 'exec plugin didn\'t return a token or client data' unless has_token
65
+ end
66
+
37
67
  def validate_credentials(opts, creds)
38
68
  # out should have ExecCredential structure
39
69
  raise 'invalid credentials' if creds.nil?
@@ -45,8 +75,7 @@ module Kubeclient
45
75
  "plugin returned version #{creds['apiVersion']}"
46
76
  end
47
77
 
48
- raise 'exec plugin didn\'t return a status field' if creds['status'].nil?
49
- raise 'exec plugin didn\'t return a token' if creds['status']['token'].nil?
78
+ validate_credentials_status(creds['status'])
50
79
  end
51
80
 
52
81
  # Transform name/value pairs to hash
@@ -1,4 +1,4 @@
1
1
  # Kubernetes REST-API Client
2
2
  module Kubeclient
3
- VERSION = '4.7.0'.freeze
3
+ VERSION = '4.12.0'.freeze
4
4
  end
@@ -79,6 +79,7 @@ module Kubeclient
79
79
  end
80
80
 
81
81
  def build_client_options
82
+ @http_options[:headers][:Authorization] = "Bearer #{File.read(@http_options[:bearer_token_file])}" if @http_options[:bearer_token_file]
82
83
  client_options = {
83
84
  headers: @http_options[:headers],
84
85
  proxy: using_proxy