kruby 1.36.0.3 → 1.36.0.4

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2368426a514cb2faa1a955a344c27535fd8a5e0ed658daa220ac3670b500a7dc
4
- data.tar.gz: 5df9b8da264349a436a6b57568f165608c2a2630475ed27280f1c5b3c8fdfd5f
3
+ metadata.gz: a427ec5d2c6e70a8edf267a9095e38b988bfef3501dc420145de416fd8aea1aa
4
+ data.tar.gz: 6ddad098a7d16fde73bf1861b2aa4c1b4dd7559821b4d238bd58474a28bc1a99
5
5
  SHA512:
6
- metadata.gz: 40a65006935258583abbe2b9111a43f6f5fa9022a7783a24c9166e0e03e9983b1243303e00d0903b893e99de105464c48b52f3040244c6cc8b02c0cab46f8c82
7
- data.tar.gz: 7a8d5684829a67ece0845578f14c2caee7f92df7a4360dc6afeb265a35ddff1803d3741f811530a8f314f59b8fac2d632ff8760cd80db855f89d8aa2a7f89fc8
6
+ metadata.gz: 1ddbe0af5eaa1ae160af1e07d7c9b54209dd064fc20362a81e93b8f709ba01feb6f4835995652db3984d27b46501cf029a3b9dfbc87252175638392f40feb761
7
+ data.tar.gz: 5959a6ec6db11ae3e323f6759add33c9c9a57cde7d1f5727c37138f2603129b2814ec5e803fe015289e9d60be2c551e135ce58353c380cf27e310e1b0ed4ab59
@@ -15,10 +15,13 @@ require 'json'
15
15
  require 'logger'
16
16
  require 'tempfile'
17
17
  require 'time'
18
+ require 'thread'
18
19
  require 'typhoeus'
19
20
 
20
21
  module Kubernetes
21
22
  class ApiClient
23
+ @default_mutex = Mutex.new
24
+
22
25
  # The Configuration object holding settings to be used in the API client.
23
26
  attr_accessor :config
24
27
 
@@ -39,7 +42,17 @@ module Kubernetes
39
42
  end
40
43
 
41
44
  def self.default
42
- @@default ||= ApiClient.new
45
+ return @@default if defined?(@@default) && @@default
46
+
47
+ @default_mutex.synchronize do
48
+ @@default ||= ApiClient.new
49
+ end
50
+ end
51
+
52
+ def self.reset_default
53
+ @default_mutex.synchronize do
54
+ @@default = nil
55
+ end
43
56
  end
44
57
 
45
58
  # Call an API with given options.
@@ -47,34 +60,43 @@ module Kubernetes
47
60
  # @return [Array<(Object, Integer, Hash)>] an array of 3 elements:
48
61
  # the data deserialized from response body (could be nil), response status code and response headers.
49
62
  def call_api(http_method, path, opts = {})
50
- request = build_request(http_method, path, opts)
51
- response = request.run
63
+ retries_performed = 0
52
64
 
53
- if @config.debugging
54
- @config.logger.debug "HTTP response body ~BEGIN~\n#{response.body}\n~END~\n"
55
- end
65
+ loop do
66
+ request = build_request(http_method, path, opts)
67
+ response = request.run
56
68
 
57
- unless response.success?
58
- if response.timed_out?
59
- fail ApiError.new('Connection timed out')
60
- elsif response.code == 0
61
- # Errors from libcurl will be made visible here
62
- fail ApiError.new(:code => 0,
63
- :message => response.return_message)
64
- else
65
- fail ApiError.new(:code => response.code,
66
- :response_headers => response.headers,
67
- :response_body => response.body),
68
- response.status_message
69
+ if @config.debugging
70
+ @config.logger.debug "HTTP response body ~BEGIN~\n#{response.body}\n~END~\n"
69
71
  end
70
- end
71
72
 
72
- if opts[:return_type]
73
- data = deserialize(response, opts[:return_type])
74
- else
75
- data = nil
73
+ unless response.success?
74
+ if retry_request?(response, retries_performed)
75
+ interval_seconds = retry_interval_seconds(response, retries_performed)
76
+ log_retry_attempt(response, retries_performed + 1, interval_seconds)
77
+ sleep interval_seconds
78
+ retries_performed += 1
79
+ next
80
+ end
81
+
82
+ if response.timed_out?
83
+ fail ApiError.new(:code => 0,
84
+ :message => 'Connection timed out')
85
+ elsif response.code == 0
86
+ # Errors from libcurl will be made visible here
87
+ fail ApiError.new(:code => 0,
88
+ :message => response.return_message)
89
+ else
90
+ fail ApiError.new(:code => response.code,
91
+ :response_headers => response.headers,
92
+ :response_body => response.body),
93
+ response.status_message
94
+ end
95
+ end
96
+
97
+ data = opts[:return_type] ? deserialize(response, opts[:return_type]) : nil
98
+ return data, response.code, response.headers
76
99
  end
77
- return data, response.code, response.headers
78
100
  end
79
101
 
80
102
  # Builds the HTTP request
@@ -225,7 +247,12 @@ module Kubernetes
225
247
  # ensuring a default content type
226
248
  content_type = response.headers['Content-Type'] || 'application/json'
227
249
 
228
- fail "Content-Type is not supported: #{content_type}" unless json_mime?(content_type)
250
+ unless json_mime?(content_type)
251
+ fail ApiError.new(:code => response.code,
252
+ :response_headers => response.headers,
253
+ :response_body => body,
254
+ :message => "Content-Type is not supported: #{content_type}")
255
+ end
229
256
 
230
257
  begin
231
258
  data = JSON.parse("[#{body}]", :symbolize_names => true)[0]
@@ -399,5 +426,85 @@ module Kubernetes
399
426
  fail "unknown collection format: #{collection_format.inspect}"
400
427
  end
401
428
  end
429
+
430
+ def retry_request?(response, retries_performed)
431
+ return false if response.timed_out?
432
+ return false if response.code.to_i == 0
433
+ return false if max_retry_attempts <= 0
434
+ return false if retries_performed >= max_retry_attempts
435
+
436
+ retry_statuses.include?(response.code.to_i)
437
+ end
438
+
439
+ def retry_interval_seconds(response, retries_performed)
440
+ exponential_interval = retry_base_interval_seconds * (2**retries_performed)
441
+ server_interval = server_retry_after_seconds(response)
442
+
443
+ server_interval ? [exponential_interval, server_interval].max : exponential_interval
444
+ end
445
+
446
+ def log_retry_attempt(response, retry_number, interval_seconds)
447
+ @config.logger.info(
448
+ "Retrying API request after HTTP #{response.code} (retry #{retry_number}/#{max_retry_attempts}) in #{interval_seconds} seconds"
449
+ )
450
+ return unless @config.debugging
451
+
452
+ @config.logger.debug("Retry response body: #{response.body}")
453
+ end
454
+
455
+ def max_retry_attempts
456
+ value = fetch_retry_configuration(:max_retries, 0)
457
+ [value.to_i, 0].max
458
+ end
459
+
460
+ def retry_base_interval_seconds
461
+ [fetch_retry_configuration(:base_interval_seconds, 1.0).to_f, 0.0].max
462
+ end
463
+
464
+ def retry_statuses
465
+ Array(fetch_retry_configuration(:retry_statuses, [429, 500, 501, 502, 503])).map(&:to_i)
466
+ end
467
+
468
+ def server_retry_after_seconds(response)
469
+ header_value = response.headers['Retry-After'] if response.respond_to?(:headers) && response.headers.is_a?(Hash)
470
+ header_seconds = parse_retry_after_header(header_value)
471
+ return header_seconds if header_seconds
472
+
473
+ parse_retry_after_from_body(response.body)
474
+ end
475
+
476
+ def parse_retry_after_header(header_value)
477
+ return nil if header_value.nil?
478
+
479
+ integer_value = Integer(header_value, exception: false)
480
+ return integer_value.to_f if integer_value
481
+
482
+ retry_at = Time.httpdate(header_value)
483
+ [retry_at - Time.now, 0.0].max
484
+ rescue ArgumentError
485
+ nil
486
+ end
487
+
488
+ def parse_retry_after_from_body(body)
489
+ return nil if body.nil? || body.empty?
490
+
491
+ parsed = JSON.parse(body)
492
+ retry_after = parsed.dig('details', 'retryAfterSeconds') || parsed.dig(:details, :retryAfterSeconds)
493
+ return nil if retry_after.nil?
494
+
495
+ retry_after.to_f
496
+ rescue JSON::ParserError, TypeError
497
+ nil
498
+ end
499
+
500
+ def fetch_retry_configuration(key, default)
501
+ return default unless @config.retry_configuration.is_a?(Hash)
502
+
503
+ if @config.retry_configuration.key?(key)
504
+ @config.retry_configuration[key]
505
+ else
506
+ @config.retry_configuration.fetch(key.to_s, default)
507
+ end
508
+ end
402
509
  end
403
510
  end
@@ -11,9 +11,12 @@ OpenAPI Generator version: 5.1.0
11
11
  =end
12
12
 
13
13
  require 'logger'
14
+ require 'thread'
14
15
 
15
16
  module Kubernetes
16
17
  class Configuration
18
+ @default_mutex = Mutex.new
19
+
17
20
  # Defines url scheme
18
21
  attr_accessor :scheme
19
22
 
@@ -88,6 +91,11 @@ module Kubernetes
88
91
  # Default to 0 (never times out).
89
92
  attr_accessor :timeout
90
93
 
94
+ # Optional retry configuration for retriable HTTP responses.
95
+ # Example:
96
+ # { max_retries: 4, base_interval_seconds: 1.0, retry_statuses: [429, 500, 501, 502, 503] }
97
+ attr_accessor :retry_configuration
98
+
91
99
  # Set this to false to skip client side validation in the operation.
92
100
  # Default to true.
93
101
  # @return [true, false]
@@ -150,6 +158,7 @@ module Kubernetes
150
158
  @api_key = {}
151
159
  @api_key_prefix = {}
152
160
  @timeout = 0
161
+ @retry_configuration = nil
153
162
  @client_side_validation = true
154
163
  @verify_ssl = true
155
164
  @verify_ssl_host = true
@@ -166,7 +175,17 @@ module Kubernetes
166
175
 
167
176
  # The default Configuration object.
168
177
  def self.default
169
- @@default ||= Configuration.new
178
+ return @@default if defined?(@@default) && @@default
179
+
180
+ @default_mutex.synchronize do
181
+ @@default ||= Configuration.new
182
+ end
183
+ end
184
+
185
+ def self.reset_default
186
+ @default_mutex.synchronize do
187
+ @@default = nil
188
+ end
170
189
  end
171
190
 
172
191
  # Backward-compatible alias used by examples and older integrations.
@@ -15,6 +15,7 @@
15
15
  require 'kubernetes/config/incluster_config'
16
16
  require 'kubernetes/config/kube_config'
17
17
  require 'rubygems/version'
18
+ require 'thread'
18
19
 
19
20
  # The Kubernetes module encapsulates the Kubernetes client for Ruby
20
21
  module Kubernetes
@@ -73,18 +74,22 @@ module Kubernetes
73
74
  end
74
75
 
75
76
  @temp_files = {}
77
+ @temp_files_mutex = Mutex.new
76
78
 
77
79
  def create_temp_file_with_base64content(content)
78
- @temp_files[content] ||= Tempfile.open('kube') do |temp|
79
- temp.write(Base64.strict_decode64(content))
80
- temp
80
+ @temp_files_mutex.synchronize do
81
+ @temp_files[content] ||= Tempfile.open('kube') do |temp|
82
+ temp.write(Base64.strict_decode64(content))
83
+ temp
84
+ end
85
+ @temp_files.fetch(content).path
81
86
  end
82
-
83
- @temp_files[content].path
84
87
  end
85
88
 
86
89
  def clear_temp_files
87
- @temp_files = {}
90
+ @temp_files_mutex.synchronize do
91
+ @temp_files = {}
92
+ end
88
93
  end
89
94
 
90
95
  SUPPORTED_KUBERNETES_VERSION_RANGE = (
@@ -11,5 +11,5 @@ OpenAPI Generator version: 5.1.0
11
11
  =end
12
12
 
13
13
  module Kubernetes
14
- VERSION = '1.36.0.3'
14
+ VERSION = '1.36.0.4'
15
15
  end
@@ -20,6 +20,9 @@ module Kubernetes
20
20
  # The Watch class provides the ability to watch a specific resource for
21
21
  # updates.
22
22
  class Watch
23
+ MAX_RECONNECT_ATTEMPTS = 3
24
+ RECONNECT_BACKOFF_SECONDS = 1.0
25
+
23
26
  def initialize(client)
24
27
  @client = client
25
28
  end
@@ -34,24 +37,50 @@ module Kubernetes
34
37
  end
35
38
 
36
39
  def connect(path, resource_version = nil, &_block)
37
- opts = { auth_names: ['BearerToken'] }
38
- url = make_url(path, resource_version)
39
- request = @client.build_request('GET', url, opts)
40
- last = ''
41
- request.on_body do |chunk|
42
- last, pieces = split_lines(last, chunk)
43
- pieces.each do |part|
44
- begin
45
- event = JSON.parse(part)
46
- rescue JSON::ParserError => e
47
- warn "Failed to parse watch event: #{e.message}. Raw event: #{part.inspect}"
40
+ current_resource_version = resource_version
41
+ reconnect_attempts = 0
42
+
43
+ loop do
44
+ reconnect_requested = false
45
+ opts = { auth_names: ['BearerToken'] }
46
+ url = make_url(path, current_resource_version)
47
+ request = @client.build_request('GET', url, opts)
48
+ last = ''
49
+
50
+ process_event = lambda do |part|
51
+ return if part.nil? || part.strip.empty?
52
+
53
+ event = parse_event(part)
54
+ return unless event
55
+
56
+ if watch_reset_event?(event)
57
+ current_resource_version = nil
58
+ reconnect_requested = true
48
59
  next
49
60
  end
50
61
 
62
+ current_resource_version = extract_resource_version(event) || current_resource_version
51
63
  yield event
52
64
  end
65
+
66
+ request.on_body do |chunk|
67
+ last, pieces = split_lines(last, chunk)
68
+ pieces.each { |part| process_event.call(part) }
69
+ end
70
+
71
+ request.on_complete do |_response|
72
+ process_event.call(last)
73
+ last = ''
74
+ end
75
+
76
+ response = request.run
77
+ reconnect_reason = reconnect_reason(response, reconnect_requested)
78
+ return response unless reconnect_reason
79
+ raise_terminal_watch_error(response, reconnect_reason) if reconnect_attempts >= MAX_RECONNECT_ATTEMPTS
80
+
81
+ reconnect_attempts += 1
82
+ sleep reconnect_backoff_seconds(reconnect_attempts - 1) if reconnect_reason == :transport
53
83
  end
54
- request.run
55
84
  end
56
85
 
57
86
  def split_lines(last, chunk)
@@ -65,5 +94,51 @@ module Kubernetes
65
94
  last = data[(ix + 1)..data.length]
66
95
  [last, complete.split(/\n/)]
67
96
  end
97
+
98
+ private
99
+
100
+ def parse_event(part)
101
+ JSON.parse(part)
102
+ rescue JSON::ParserError => e
103
+ warn "Failed to parse watch event: #{e.message}. Raw event: #{part.inspect}"
104
+ nil
105
+ end
106
+
107
+ def extract_resource_version(event)
108
+ object = event['object']
109
+ return nil unless object.is_a?(Hash)
110
+
111
+ metadata = object['metadata']
112
+ return metadata['resourceVersion'] if metadata.is_a?(Hash)
113
+
114
+ object['resourceVersion']
115
+ end
116
+
117
+ def watch_reset_event?(event)
118
+ event['type'] == 'ERROR' && event.dig('object', 'code').to_i == 410
119
+ end
120
+
121
+ def reconnect_reason(response, reconnect_requested)
122
+ return :reset if reconnect_requested
123
+ return nil unless response
124
+
125
+ return :transport if response.timed_out? || response.code.to_i.zero?
126
+
127
+ nil
128
+ end
129
+
130
+ def reconnect_backoff_seconds(reconnect_attempt)
131
+ RECONNECT_BACKOFF_SECONDS * (2**reconnect_attempt)
132
+ end
133
+
134
+ def raise_terminal_watch_error(response, reconnect_reason)
135
+ if reconnect_reason == :reset
136
+ raise ApiError.new(code: 410, message: 'Watch resource version expired')
137
+ elsif response&.timed_out?
138
+ raise ApiError.new('Watch connection timed out')
139
+ elsif response && response.code.to_i.zero?
140
+ raise ApiError.new(code: 0, message: response.return_message)
141
+ end
142
+ end
68
143
  end
69
144
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: kruby
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.36.0.3
4
+ version: 1.36.0.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - doridoridoriand