kubeclient 4.2.2 → 4.7.0

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.

@@ -20,7 +20,10 @@ Edit `CHANGELOG.md` as necessary. Even if all included changes remembered to up
20
20
 
21
21
  Bump `lib/kubeclient/version.rb` manually, or by using:
22
22
  ```bash
23
- git checkout -b release-$RELEASE_VERSION
23
+ RELEASE_VERSION=x.y.z
24
+
25
+ git checkout -b "release-$RELEASE_VERSION" $RELEASE_BRANCH
26
+ # Won't work with uncommitted changes, you have to commit the changelog first.
24
27
  gem bump --version $RELEASE_VERSION
25
28
  git show # View version bump change.
26
29
  ```
@@ -20,7 +20,7 @@ Gem::Specification.new do |spec|
20
20
  spec.require_paths = ['lib']
21
21
  spec.required_ruby_version = '>= 2.2.0'
22
22
 
23
- spec.add_development_dependency 'bundler', '~> 1.6'
23
+ spec.add_development_dependency 'bundler', '>= 1.6'
24
24
  spec.add_development_dependency 'rake', '~> 12.0'
25
25
  spec.add_development_dependency 'minitest'
26
26
  spec.add_development_dependency 'minitest-rg'
@@ -28,8 +28,11 @@ Gem::Specification.new do |spec|
28
28
  spec.add_development_dependency 'vcr'
29
29
  spec.add_development_dependency 'rubocop', '= 0.49.1'
30
30
  spec.add_development_dependency 'googleauth', '~> 0.5.1'
31
+ spec.add_development_dependency('mocha', '~> 1.5')
32
+ spec.add_development_dependency 'openid_connect', '~> 1.1'
33
+ spec.add_development_dependency 'jsonpath', '~> 1.0'
31
34
 
32
35
  spec.add_dependency 'rest-client', '~> 2.0'
33
- spec.add_dependency 'recursive-open-struct', '~> 1.0', '>= 1.0.4'
34
- spec.add_dependency 'http', '~> 3.0'
36
+ spec.add_dependency 'recursive-open-struct', '~> 1.1', '>= 1.1.1'
37
+ spec.add_dependency 'http', '>= 3.0', '< 5.0'
35
38
  end
@@ -1,13 +1,15 @@
1
1
  require 'json'
2
2
  require 'rest-client'
3
3
 
4
+ require 'kubeclient/aws_eks_credentials'
4
5
  require 'kubeclient/common'
5
6
  require 'kubeclient/config'
6
7
  require 'kubeclient/entity_list'
7
- require 'kubeclient/google_application_default_credentials'
8
8
  require 'kubeclient/exec_credentials'
9
+ require 'kubeclient/gcp_auth_provider'
9
10
  require 'kubeclient/http_error'
10
11
  require 'kubeclient/missing_kind_compatibility'
12
+ require 'kubeclient/oidc_auth_provider'
11
13
  require 'kubeclient/resource'
12
14
  require 'kubeclient/resource_not_found_error'
13
15
  require 'kubeclient/version'
@@ -26,7 +28,7 @@ module Kubeclient
26
28
  uri,
27
29
  '/api',
28
30
  version,
29
- options
31
+ **options
30
32
  )
31
33
  end
32
34
  end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kubeclient
4
+ # Get a bearer token to authenticate against aws eks.
5
+ class AmazonEksCredentials
6
+ class AmazonEksDependencyError < LoadError # rubocop:disable Lint/InheritException
7
+ end
8
+
9
+ class << self
10
+ def token(credentials, eks_cluster)
11
+ begin
12
+ require 'aws-sigv4'
13
+ require 'base64'
14
+ require 'cgi'
15
+ rescue LoadError => e
16
+ raise AmazonEksDependencyError,
17
+ 'Error requiring aws gems. Kubeclient itself does not include the following ' \
18
+ 'gems: [aws-sigv4]. To support auth-provider eks, you must ' \
19
+ "include it in your calling application. Failed with: #{e.message}"
20
+ end
21
+ # https://github.com/aws/aws-sdk-ruby/pull/1848
22
+ # Get a signer
23
+ # Note - sts only has ONE endpoint (not regional) so 'us-east-1' hardcoding should be OK
24
+ signer = Aws::Sigv4::Signer.new(
25
+ service: 'sts',
26
+ region: 'us-east-1',
27
+ credentials: credentials
28
+ )
29
+
30
+ # https://docs.aws.amazon.com/sdk-for-ruby/v3/api/Aws/Sigv4/Signer.html#presign_url-instance_method
31
+ presigned_url_string = signer.presign_url(
32
+ http_method: 'GET',
33
+ url: 'https://sts.amazonaws.com/?Action=GetCallerIdentity&Version=2011-06-15',
34
+ body: '',
35
+ credentials: credentials,
36
+ expires_in: 60,
37
+ headers: {
38
+ 'X-K8s-Aws-Id' => eks_cluster
39
+ }
40
+ )
41
+ kube_token = 'k8s-aws-v1.' + Base64.urlsafe_encode64(presigned_url_string.to_s).sub(/=*$/, '') # rubocop:disable Metrics/LineLength
42
+ kube_token
43
+ end
44
+ end
45
+ end
46
+ end
@@ -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].freeze
8
+ ENTITY_METHODS = %w[get watch delete create update patch json_patch merge_patch].freeze
9
9
 
10
10
  DEFAULT_SSL_OPTIONS = {
11
11
  client_cert: nil,
@@ -37,10 +37,11 @@ module Kubeclient
37
37
  DEFAULT_HTTP_MAX_REDIRECTS = 10
38
38
 
39
39
  SEARCH_ARGUMENTS = {
40
- 'labelSelector' => :label_selector,
41
- 'fieldSelector' => :field_selector,
42
- 'limit' => :limit,
43
- 'continue' => :continue
40
+ 'labelSelector' => :label_selector,
41
+ 'fieldSelector' => :field_selector,
42
+ 'resourceVersion' => :resource_version,
43
+ 'limit' => :limit,
44
+ 'continue' => :continue
44
45
  }.freeze
45
46
 
46
47
  WATCH_ARGUMENTS = {
@@ -203,6 +204,7 @@ module Kubeclient
203
204
  namespace.to_s.empty? ? '' : "namespaces/#{namespace}/"
204
205
  end
205
206
 
207
+ # rubocop:disable Metrics/BlockLength
206
208
  def define_entity_methods
207
209
  @entities.values.each do |entity|
208
210
  # get all entities of a type e.g. get_nodes, get_pods, etc.
@@ -211,12 +213,12 @@ module Kubeclient
211
213
  end
212
214
 
213
215
  # watch all entities of a type e.g. watch_nodes, watch_pods, etc.
214
- define_singleton_method("watch_#{entity.method_names[1]}") do |options = {}|
216
+ define_singleton_method("watch_#{entity.method_names[1]}") do |options = {}, &block|
215
217
  # This method used to take resource_version as a param, so
216
218
  # this conversion is to keep backwards compatibility
217
219
  options = { resource_version: options } unless options.is_a?(Hash)
218
220
 
219
- watch_entities(entity.resource_name, options)
221
+ watch_entities(entity.resource_name, options, &block)
220
222
  end
221
223
 
222
224
  # get a single entity of a specific type by name
@@ -227,7 +229,7 @@ module Kubeclient
227
229
 
228
230
  define_singleton_method("delete_#{entity.method_names[0]}") \
229
231
  do |name, namespace = nil, opts = {}|
230
- delete_entity(entity.resource_name, name, namespace, opts)
232
+ delete_entity(entity.resource_name, name, namespace, **opts)
231
233
  end
232
234
 
233
235
  define_singleton_method("create_#{entity.method_names[0]}") do |entity_config|
@@ -238,11 +240,23 @@ module Kubeclient
238
240
  update_entity(entity.resource_name, entity_config)
239
241
  end
240
242
 
241
- define_singleton_method("patch_#{entity.method_names[0]}") do |name, patch, namespace = nil|
242
- patch_entity(entity.resource_name, name, patch, namespace)
243
+ define_singleton_method("patch_#{entity.method_names[0]}") \
244
+ do |name, patch, namespace = nil|
245
+ patch_entity(entity.resource_name, name, patch, 'strategic-merge-patch', namespace)
246
+ end
247
+
248
+ define_singleton_method("json_patch_#{entity.method_names[0]}") \
249
+ do |name, patch, namespace = nil|
250
+ patch_entity(entity.resource_name, name, patch, 'json-patch', namespace)
251
+ end
252
+
253
+ define_singleton_method("merge_patch_#{entity.method_names[0]}") \
254
+ do |name, patch, namespace = nil|
255
+ patch_entity(entity.resource_name, name, patch, 'merge-patch', namespace)
243
256
  end
244
257
  end
245
258
  end
259
+ # rubocop:enable Metrics/BlockLength
246
260
 
247
261
  def self.underscore_entity(entity_name)
248
262
  entity_name.gsub(/([a-z])([A-Z])/, '\1_\2').downcase
@@ -281,7 +295,9 @@ module Kubeclient
281
295
  # :as (:raw|:ros) - defaults to :ros
282
296
  # :raw - return the raw response body as a string
283
297
  # :ros - return a collection of RecursiveOpenStruct objects
284
- def watch_entities(resource_name, options = {})
298
+ # Accepts an optional block, that will be called with each entity,
299
+ # otherwise returns a WatchStream
300
+ def watch_entities(resource_name, options = {}, &block)
285
301
  ns = build_namespace_prefix(options[:namespace])
286
302
 
287
303
  path = "watch/#{ns}#{resource_name}"
@@ -292,11 +308,13 @@ module Kubeclient
292
308
  WATCH_ARGUMENTS.each { |k, v| params[k] = options[v] if options[v] }
293
309
  uri.query = URI.encode_www_form(params) if params.any?
294
310
 
295
- Kubeclient::Common::WatchStream.new(
311
+ watcher = Kubeclient::Common::WatchStream.new(
296
312
  uri,
297
313
  http_options(uri),
298
314
  formatter: ->(value) { format_response(options[:as] || @as, value) }
299
315
  )
316
+
317
+ return_or_yield_to_watcher(watcher, &block)
300
318
  end
301
319
 
302
320
  # Accepts the following options:
@@ -381,13 +399,13 @@ module Kubeclient
381
399
  format_response(@as, response.body)
382
400
  end
383
401
 
384
- def patch_entity(resource_name, name, patch, namespace = nil)
402
+ def patch_entity(resource_name, name, patch, strategy, namespace)
385
403
  ns_prefix = build_namespace_prefix(namespace)
386
404
  response = handle_exception do
387
405
  rest_client[ns_prefix + resource_name + "/#{name}"]
388
406
  .patch(
389
407
  patch.to_json,
390
- { 'Content-Type' => 'application/strategic-merge-patch+json' }.merge(@headers)
408
+ { 'Content-Type' => "application/#{strategy}+json" }.merge(@headers)
391
409
  )
392
410
  end
393
411
  format_response(@as, response.body)
@@ -409,13 +427,14 @@ module Kubeclient
409
427
 
410
428
  def get_pod_log(pod_name, namespace,
411
429
  container: nil, previous: false,
412
- timestamps: false, since_time: nil, tail_lines: nil)
430
+ timestamps: false, since_time: nil, tail_lines: nil, limit_bytes: nil)
413
431
  params = {}
414
432
  params[:previous] = true if previous
415
433
  params[:container] = container if container
416
434
  params[:timestamps] = timestamps if timestamps
417
435
  params[:sinceTime] = format_datetime(since_time) if since_time
418
436
  params[:tailLines] = tail_lines if tail_lines
437
+ params[:limitBytes] = limit_bytes if limit_bytes
419
438
 
420
439
  ns = build_namespace_prefix(namespace)
421
440
  handle_exception do
@@ -424,7 +443,7 @@ module Kubeclient
424
443
  end
425
444
  end
426
445
 
427
- def watch_pod_log(pod_name, namespace, container: nil)
446
+ def watch_pod_log(pod_name, namespace, container: nil, &block)
428
447
  # Adding the "follow=true" query param tells the Kubernetes API to keep
429
448
  # the connection open and stream updates to the log.
430
449
  params = { follow: true }
@@ -436,7 +455,10 @@ module Kubeclient
436
455
  uri.path += "/#{@api_version}/#{ns}pods/#{pod_name}/log"
437
456
  uri.query = URI.encode_www_form(params)
438
457
 
439
- Kubeclient::Common::WatchStream.new(uri, http_options(uri), formatter: ->(value) { value })
458
+ watcher = Kubeclient::Common::WatchStream.new(
459
+ uri, http_options(uri), formatter: ->(value) { value }
460
+ )
461
+ return_or_yield_to_watcher(watcher, &block)
440
462
  end
441
463
 
442
464
  def proxy_url(kind, name, port, namespace = '')
@@ -573,6 +595,16 @@ module Kubeclient
573
595
  raise ArgumentError, msg unless File.readable?(@auth_options[:bearer_token_file])
574
596
  end
575
597
 
598
+ def return_or_yield_to_watcher(watcher, &block)
599
+ return watcher unless block_given?
600
+
601
+ begin
602
+ watcher.each(&block)
603
+ ensure
604
+ watcher.finish
605
+ end
606
+ end
607
+
576
608
  def http_options(uri)
577
609
  options = {
578
610
  basic_auth_user: @auth_options[:username],
@@ -150,9 +150,10 @@ module Kubeclient
150
150
  if user.key?('token')
151
151
  options[:bearer_token] = user['token']
152
152
  elsif user.key?('exec')
153
- exec_opts = user['exec'].dup
154
- exec_opts['command'] = ext_command_path(exec_opts['command']) if exec_opts['command']
153
+ exec_opts = expand_command_option(user['exec'], 'command')
155
154
  options[:bearer_token] = Kubeclient::ExecCredentials.token(exec_opts)
155
+ elsif user.key?('auth-provider')
156
+ options[:bearer_token] = fetch_token_from_provider(user['auth-provider'])
156
157
  else
157
158
  %w[username password].each do |attr|
158
159
  options[attr.to_sym] = user[attr] if user.key?(attr)
@@ -160,5 +161,22 @@ module Kubeclient
160
161
  end
161
162
  options
162
163
  end
164
+
165
+ def fetch_token_from_provider(auth_provider)
166
+ case auth_provider['name']
167
+ when 'gcp'
168
+ config = expand_command_option(auth_provider['config'], 'cmd-path')
169
+ Kubeclient::GCPAuthProvider.token(config)
170
+ when 'oidc'
171
+ Kubeclient::OIDCAuthProvider.token(auth_provider['config'])
172
+ end
173
+ end
174
+
175
+ def expand_command_option(config, key)
176
+ config = config.dup
177
+ config[key] = ext_command_path(config[key]) if config[key]
178
+
179
+ config
180
+ end
163
181
  end
164
182
  end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'kubeclient/google_application_default_credentials'
4
+ require 'kubeclient/gcp_command_credentials'
5
+
6
+ module Kubeclient
7
+ # Handle different ways to get a bearer token for Google Cloud Platform.
8
+ class GCPAuthProvider
9
+ class << self
10
+ def token(config)
11
+ if config.key?('cmd-path')
12
+ Kubeclient::GCPCommandCredentials.token(config)
13
+ else
14
+ Kubeclient::GoogleApplicationDefaultCredentials.token
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kubeclient
4
+ # Generates a bearer token for Google Cloud Platform.
5
+ class GCPCommandCredentials
6
+ class << self
7
+ def token(config)
8
+ require 'open3'
9
+ require 'shellwords'
10
+ require 'json'
11
+ require 'jsonpath'
12
+
13
+ cmd = config['cmd-path']
14
+ args = config['cmd-args']
15
+ token_key = config['token-key']
16
+
17
+ out, err, st = Open3.capture3(cmd, *args.split(' '))
18
+
19
+ raise "exec command failed: #{err}" unless st.success?
20
+
21
+ extract_token(out, token_key)
22
+ end
23
+
24
+ private
25
+
26
+ def extract_token(output, token_key)
27
+ JsonPath.on(output, token_key.gsub(/^{|}$/, '')).first
28
+ end
29
+ end
30
+ end
31
+ end
@@ -3,10 +3,25 @@
3
3
  module Kubeclient
4
4
  # Get a bearer token from the Google's application default credentials.
5
5
  class GoogleApplicationDefaultCredentials
6
+ class GoogleDependencyError < LoadError # rubocop:disable Lint/InheritException
7
+ end
8
+
6
9
  class << self
7
10
  def token
8
- require 'googleauth'
9
- scopes = ['https://www.googleapis.com/auth/cloud-platform']
11
+ begin
12
+ require 'googleauth'
13
+ rescue LoadError => e
14
+ raise GoogleDependencyError,
15
+ 'Error requiring googleauth gem. Kubeclient itself does not include the ' \
16
+ 'googleauth gem. To support auth-provider gcp, you must include it in your ' \
17
+ "calling application. Failed with: #{e.message}"
18
+ end
19
+
20
+ scopes = [
21
+ 'https://www.googleapis.com/auth/cloud-platform',
22
+ 'https://www.googleapis.com/auth/userinfo.email'
23
+ ]
24
+
10
25
  authorization = Google::Auth.get_application_default(scopes)
11
26
  authorization.apply({})
12
27
  authorization.access_token
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kubeclient
4
+ # Uses OIDC id-tokens and refreshes them if they are stale.
5
+ class OIDCAuthProvider
6
+ class OpenIDConnectDependencyError < LoadError # rubocop:disable Lint/InheritException
7
+ end
8
+
9
+ class << self
10
+ def token(provider_config)
11
+ begin
12
+ require 'openid_connect'
13
+ rescue LoadError => e
14
+ raise OpenIDConnectDependencyError,
15
+ 'Error requiring openid_connect gem. Kubeclient itself does not include the ' \
16
+ 'openid_connect gem. To support auth-provider oidc, you must include it in your ' \
17
+ "calling application. Failed with: #{e.message}"
18
+ end
19
+
20
+ issuer_url = provider_config['idp-issuer-url']
21
+ discovery = OpenIDConnect::Discovery::Provider::Config.discover! issuer_url
22
+
23
+ if provider_config.key? 'id-token'
24
+ return provider_config['id-token'] unless expired?(provider_config['id-token'], discovery)
25
+ end
26
+
27
+ client = OpenIDConnect::Client.new(
28
+ identifier: provider_config['client-id'],
29
+ secret: provider_config['client-secret'],
30
+ authorization_endpoint: discovery.authorization_endpoint,
31
+ token_endpoint: discovery.token_endpoint,
32
+ userinfo_endpoint: discovery.userinfo_endpoint
33
+ )
34
+ client.refresh_token = provider_config['refresh-token']
35
+ client.access_token!.id_token
36
+ end
37
+
38
+ def expired?(id_token, discovery)
39
+ decoded_token = OpenIDConnect::ResponseObject::IdToken.decode(
40
+ id_token,
41
+ discovery.jwks
42
+ )
43
+ # If token expired or expiring within 60 seconds
44
+ Time.now.to_i + 60 > decoded_token.exp.to_i
45
+ rescue JSON::JWK::Set::KidNotFound
46
+ # Token cannot be verified: the kid it was signed with is not available for discovery
47
+ # Consider it expired and fetch a new one.
48
+ true
49
+ end
50
+ end
51
+ end
52
+ end