kruby 1.36.0.2 → 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/config/incluster_config.rb +2 -2
- data/lib/kubernetes/config/kube_config.rb +13 -2
- data/lib/kubernetes/configuration.rb +35 -7
- data/lib/kubernetes/release/publish_guard.rb +63 -0
- data/lib/kubernetes/utils.rb +11 -6
- data/lib/kubernetes/version.rb +1 -1
- data/lib/kubernetes/watch.rb +94 -15
- metadata +2 -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
|
|
@@ -90,13 +90,13 @@ module Kubernetes
|
|
|
90
90
|
Configuration.instance_variable_set(:@in_cluster_config, self)
|
|
91
91
|
Configuration.prepend(Module.new do
|
|
92
92
|
# rubocop:disable Metrics/LineLength
|
|
93
|
-
def api_key_with_prefix(identifier)
|
|
93
|
+
def api_key_with_prefix(identifier, param_alias = nil)
|
|
94
94
|
in_cluster_config = self.class.instance_variable_get(:@in_cluster_config)
|
|
95
95
|
if identifier == 'authorization' && @api_key.key?(identifier) && in_cluster_config.token_expires_at <= Time.now
|
|
96
96
|
in_cluster_config.load_token
|
|
97
97
|
@api_key[identifier] = 'Bearer ' + in_cluster_config.token
|
|
98
98
|
end
|
|
99
|
-
super identifier
|
|
99
|
+
super identifier, param_alias
|
|
100
100
|
end
|
|
101
101
|
# rubocop:enable Metrics/LineLength
|
|
102
102
|
end)
|
|
@@ -49,6 +49,13 @@ module Kubernetes
|
|
|
49
49
|
File.dirname(path)
|
|
50
50
|
end
|
|
51
51
|
|
|
52
|
+
def resolve_path(file_path)
|
|
53
|
+
return file_path unless file_path
|
|
54
|
+
file_path = file_path.to_s
|
|
55
|
+
return file_path if file_path.start_with?('/')
|
|
56
|
+
File.expand_path(file_path, File.expand_path(base_path))
|
|
57
|
+
end
|
|
58
|
+
|
|
52
59
|
def config
|
|
53
60
|
@config ||= File.open(path) do |io|
|
|
54
61
|
::YAML.safe_load(io.read)
|
|
@@ -86,8 +93,8 @@ module Kubernetes
|
|
|
86
93
|
|
|
87
94
|
def setup_ssl(cluster, user, config)
|
|
88
95
|
# rubocop:disable DoubleNegation
|
|
89
|
-
config.verify_ssl = !!cluster['
|
|
90
|
-
config.verify_ssl_host = !!cluster['
|
|
96
|
+
config.verify_ssl = !!cluster['verify_ssl']
|
|
97
|
+
config.verify_ssl_host = !!cluster['verify_ssl']
|
|
91
98
|
# rubocop:enable DoubleNegation
|
|
92
99
|
|
|
93
100
|
config.ssl_ca_cert = cluster['certificate-authority']
|
|
@@ -98,6 +105,7 @@ module Kubernetes
|
|
|
98
105
|
def find_cluster(name)
|
|
99
106
|
find_by_name(config['clusters'], 'cluster', name).tap do |cluster|
|
|
100
107
|
Kubernetes.create_temp_file_and_set(cluster, 'certificate-authority')
|
|
108
|
+
cluster['certificate-authority'] = resolve_path(cluster['certificate-authority'])
|
|
101
109
|
cluster['verify_ssl'] = !cluster['insecure-skip-tls-verify']
|
|
102
110
|
end
|
|
103
111
|
end
|
|
@@ -108,6 +116,8 @@ module Kubernetes
|
|
|
108
116
|
|
|
109
117
|
Kubernetes.create_temp_file_and_set(user, 'client-certificate')
|
|
110
118
|
Kubernetes.create_temp_file_and_set(user, 'client-key')
|
|
119
|
+
user['client-certificate'] = resolve_path(user['client-certificate'])
|
|
120
|
+
user['client-key'] = resolve_path(user['client-key'])
|
|
111
121
|
load_token_file(user)
|
|
112
122
|
setup_auth(user)
|
|
113
123
|
end
|
|
@@ -117,6 +127,7 @@ module Kubernetes
|
|
|
117
127
|
# If tokenFile is specified, then set token
|
|
118
128
|
return unless !user['token'] && user['tokenFile']
|
|
119
129
|
|
|
130
|
+
user['tokenFile'] = resolve_path(user['tokenFile'])
|
|
120
131
|
File.open(user['tokenFile']) do |io|
|
|
121
132
|
user['token'] = io.read.chomp
|
|
122
133
|
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.
|
|
@@ -260,21 +279,30 @@ module Kubernetes
|
|
|
260
279
|
end
|
|
261
280
|
|
|
262
281
|
server = servers[index]
|
|
263
|
-
url = server[:url]
|
|
282
|
+
url = server[:url].dup
|
|
264
283
|
|
|
265
284
|
return url unless server.key? :variables
|
|
266
285
|
|
|
267
286
|
# go through variable and assign a value
|
|
268
287
|
server[:variables].each do |name, variable|
|
|
269
|
-
if variables.key?(name)
|
|
270
|
-
|
|
271
|
-
|
|
288
|
+
variable_key = if variables.key?(name)
|
|
289
|
+
name
|
|
290
|
+
elsif variables.key?(name.to_s)
|
|
291
|
+
name.to_s
|
|
292
|
+
elsif name.respond_to?(:to_sym) && variables.key?(name.to_sym)
|
|
293
|
+
name.to_sym
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
if variable_key
|
|
297
|
+
variable_value = variables[variable_key]
|
|
298
|
+
if (!variable.key?(:enum_values) || variable[:enum_values].include?(variable_value))
|
|
299
|
+
url.gsub! "{" + name.to_s + "}", variable_value
|
|
272
300
|
else
|
|
273
|
-
fail ArgumentError, "The variable `#{name}` in the server URL has invalid value #{
|
|
301
|
+
fail ArgumentError, "The variable `#{name}` in the server URL has invalid value #{variable_value}. Must be #{variable[:enum_values]}."
|
|
274
302
|
end
|
|
275
303
|
else
|
|
276
304
|
# use default value
|
|
277
|
-
url.gsub! "{" + name.to_s + "}",
|
|
305
|
+
url.gsub! "{" + name.to_s + "}", variable[:default_value]
|
|
278
306
|
end
|
|
279
307
|
end
|
|
280
308
|
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "open3"
|
|
4
|
+
require "rubygems/specification"
|
|
5
|
+
require "shellwords"
|
|
6
|
+
require "kubernetes/release/changelog"
|
|
7
|
+
|
|
8
|
+
module Kubernetes
|
|
9
|
+
module Release
|
|
10
|
+
module PublishGuard
|
|
11
|
+
module_function
|
|
12
|
+
|
|
13
|
+
def gemspec_files(gemspec_path)
|
|
14
|
+
spec = Gem::Specification.load(gemspec_path)
|
|
15
|
+
raise Changelog::Error, "failed to load gemspec #{gemspec_path}" unless spec
|
|
16
|
+
|
|
17
|
+
spec.files
|
|
18
|
+
rescue Gem::InvalidSpecificationException => e
|
|
19
|
+
raise Changelog::Error, e.message
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def untracked_package_files(repo_root:, package_root:, package_files:)
|
|
23
|
+
repo_paths = package_files.map do |path|
|
|
24
|
+
package_file_repo_path(repo_root: repo_root, package_root: package_root, package_path: path)
|
|
25
|
+
end.uniq.sort
|
|
26
|
+
return [] if repo_paths.empty?
|
|
27
|
+
|
|
28
|
+
tracked_paths = capture_command("git", "-C", repo_root, "ls-files", "--", *repo_paths).lines.map(&:chomp)
|
|
29
|
+
repo_paths - tracked_paths
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def untracked_gemspec_files(repo_root:, package_root:, gemspec_path:)
|
|
33
|
+
untracked_package_files(
|
|
34
|
+
repo_root: repo_root,
|
|
35
|
+
package_root: package_root,
|
|
36
|
+
package_files: gemspec_files(gemspec_path)
|
|
37
|
+
)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def package_file_repo_path(repo_root:, package_root:, package_path:)
|
|
41
|
+
repo_root = File.expand_path(repo_root)
|
|
42
|
+
repo_prefix = "#{repo_root}#{File::SEPARATOR}"
|
|
43
|
+
full_path = File.expand_path(package_path, package_root)
|
|
44
|
+
|
|
45
|
+
unless full_path.start_with?(repo_prefix)
|
|
46
|
+
raise Changelog::Error, "gem package file #{package_path} resolves outside #{repo_root}"
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
full_path.delete_prefix(repo_prefix)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def capture_command(*command)
|
|
53
|
+
output, status = Open3.capture2e(*command)
|
|
54
|
+
return output if status.success?
|
|
55
|
+
|
|
56
|
+
message = "command failed: #{Shellwords.join(command)}"
|
|
57
|
+
details = output.strip
|
|
58
|
+
message = "#{message}\n#{details}" unless details.empty?
|
|
59
|
+
raise Changelog::Error, message
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
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
|
@@ -13,41 +13,74 @@
|
|
|
13
13
|
# limitations under the License.
|
|
14
14
|
|
|
15
15
|
require 'json'
|
|
16
|
+
require 'uri'
|
|
16
17
|
|
|
17
18
|
# The Kubernetes module encapsulates the Kubernetes client for Ruby
|
|
18
19
|
module Kubernetes
|
|
19
20
|
# The Watch class provides the ability to watch a specific resource for
|
|
20
21
|
# updates.
|
|
21
22
|
class Watch
|
|
23
|
+
MAX_RECONNECT_ATTEMPTS = 3
|
|
24
|
+
RECONNECT_BACKOFF_SECONDS = 1.0
|
|
25
|
+
|
|
22
26
|
def initialize(client)
|
|
23
27
|
@client = client
|
|
24
28
|
end
|
|
25
29
|
|
|
26
30
|
def make_url(path, resource_version)
|
|
27
|
-
|
|
28
|
-
query
|
|
29
|
-
|
|
31
|
+
uri = URI.parse(path)
|
|
32
|
+
query = URI.decode_www_form(uri.query || '').to_h
|
|
33
|
+
query['watch'] = 'true'
|
|
34
|
+
query['resourceVersion'] = resource_version if resource_version
|
|
35
|
+
query_string = query.map { |k, v| "#{URI.encode_www_form_component(k).gsub('+', '%20')}=#{URI.encode_www_form_component(v).gsub('+', '%20')}" }.join('&')
|
|
36
|
+
"#{uri.path}?#{query_string}"
|
|
30
37
|
end
|
|
31
38
|
|
|
32
39
|
def connect(path, resource_version = nil, &_block)
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
|
44
59
|
next
|
|
45
60
|
end
|
|
46
61
|
|
|
62
|
+
current_resource_version = extract_resource_version(event) || current_resource_version
|
|
47
63
|
yield event
|
|
48
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
|
|
49
83
|
end
|
|
50
|
-
request.run
|
|
51
84
|
end
|
|
52
85
|
|
|
53
86
|
def split_lines(last, chunk)
|
|
@@ -61,5 +94,51 @@ module Kubernetes
|
|
|
61
94
|
last = data[(ix + 1)..data.length]
|
|
62
95
|
[last, complete.split(/\n/)]
|
|
63
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
|
|
64
143
|
end
|
|
65
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.
|
|
4
|
+
version: 1.36.0.4
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- doridoridoriand
|
|
@@ -903,6 +903,7 @@ files:
|
|
|
903
903
|
- lib/kubernetes/models/v2_resource_metric_status.rb
|
|
904
904
|
- lib/kubernetes/models/version_info.rb
|
|
905
905
|
- lib/kubernetes/release/changelog.rb
|
|
906
|
+
- lib/kubernetes/release/publish_guard.rb
|
|
906
907
|
- lib/kubernetes/utils.rb
|
|
907
908
|
- lib/kubernetes/version.rb
|
|
908
909
|
- lib/kubernetes/watch.rb
|