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 +4 -4
- data/lib/kubernetes/api_client.rb +132 -25
- data/lib/kubernetes/configuration.rb +20 -1
- data/lib/kubernetes/utils.rb +11 -6
- data/lib/kubernetes/version.rb +1 -1
- data/lib/kubernetes/watch.rb +87 -12
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a427ec5d2c6e70a8edf267a9095e38b988bfef3501dc420145de416fd8aea1aa
|
|
4
|
+
data.tar.gz: 6ddad098a7d16fde73bf1861b2aa4c1b4dd7559821b4d238bd58474a28bc1a99
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
|
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
|
-
|
|
51
|
-
response = request.run
|
|
63
|
+
retries_performed = 0
|
|
52
64
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
65
|
+
loop do
|
|
66
|
+
request = build_request(http_method, path, opts)
|
|
67
|
+
response = request.run
|
|
56
68
|
|
|
57
|
-
|
|
58
|
-
|
|
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
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
data/lib/kubernetes/utils.rb
CHANGED
|
@@ -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
|
-
@
|
|
79
|
-
|
|
80
|
-
|
|
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
|
-
@
|
|
90
|
+
@temp_files_mutex.synchronize do
|
|
91
|
+
@temp_files = {}
|
|
92
|
+
end
|
|
88
93
|
end
|
|
89
94
|
|
|
90
95
|
SUPPORTED_KUBERNETES_VERSION_RANGE = (
|
data/lib/kubernetes/version.rb
CHANGED
data/lib/kubernetes/watch.rb
CHANGED
|
@@ -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
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|