kubeclient 0.3.0 → 4.9.2
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.
- checksums.yaml +5 -5
- data/.github/workflows/actions.yml +35 -0
- data/.gitignore +1 -0
- data/.rubocop.yml +29 -0
- data/CHANGELOG.md +208 -0
- data/Gemfile +3 -0
- data/README.md +706 -57
- data/RELEASING.md +69 -0
- data/Rakefile +3 -5
- data/kubeclient.gemspec +19 -11
- data/lib/kubeclient/aws_eks_credentials.rb +46 -0
- data/lib/kubeclient/common.rb +597 -161
- data/lib/kubeclient/config.rb +195 -0
- data/lib/kubeclient/entity_list.rb +7 -2
- data/lib/kubeclient/exec_credentials.rb +89 -0
- data/lib/kubeclient/gcp_auth_provider.rb +19 -0
- data/lib/kubeclient/gcp_command_credentials.rb +31 -0
- data/lib/kubeclient/google_application_default_credentials.rb +31 -0
- data/lib/kubeclient/http_error.rb +25 -0
- data/lib/kubeclient/missing_kind_compatibility.rb +68 -0
- data/lib/kubeclient/oidc_auth_provider.rb +52 -0
- data/lib/kubeclient/resource.rb +11 -0
- data/lib/kubeclient/resource_not_found_error.rb +4 -0
- data/lib/kubeclient/version.rb +1 -1
- data/lib/kubeclient/watch_stream.rb +71 -28
- data/lib/kubeclient.rb +25 -82
- metadata +140 -114
- data/.travis.yml +0 -6
- data/lib/kubeclient/kube_exception.rb +0 -13
- data/lib/kubeclient/watch_notice.rb +0 -7
- data/test/json/created_namespace_b3.json +0 -20
- data/test/json/created_secret.json +0 -16
- data/test/json/created_service_b3.json +0 -31
- data/test/json/empty_pod_list_b3.json +0 -9
- data/test/json/endpoint_list_b3.json +0 -48
- data/test/json/entity_list_b3.json +0 -56
- data/test/json/event_list_b3.json +0 -35
- data/test/json/namespace_b3.json +0 -13
- data/test/json/namespace_exception_b3.json +0 -8
- data/test/json/namespace_list_b3.json +0 -32
- data/test/json/node_b3.json +0 -29
- data/test/json/node_list_b3.json +0 -37
- data/test/json/pod_b3.json +0 -92
- data/test/json/pod_list_b3.json +0 -75
- data/test/json/replication_controller_b3.json +0 -57
- data/test/json/replication_controller_list_b3.json +0 -64
- data/test/json/secret_list_b3.json +0 -44
- data/test/json/service_b3.json +0 -33
- data/test/json/service_illegal_json_404.json +0 -1
- data/test/json/service_list_b3.json +0 -97
- data/test/json/service_update_b3.json +0 -22
- data/test/json/versions_list.json +0 -6
- data/test/json/watch_stream_b3.json +0 -3
- data/test/test_helper.rb +0 -4
- data/test/test_kubeclient.rb +0 -407
- data/test/test_namespace.rb +0 -53
- data/test/test_node.rb +0 -25
- data/test/test_pod.rb +0 -21
- data/test/test_replication_controller.rb +0 -24
- data/test/test_secret.rb +0 -58
- data/test/test_service.rb +0 -136
- data/test/test_watch.rb +0 -37
- data/test/valid_token_file +0 -1
data/lib/kubeclient/common.rb
CHANGED
@@ -1,225 +1,661 @@
|
|
1
1
|
require 'json'
|
2
2
|
require 'rest-client'
|
3
|
+
|
3
4
|
module Kubeclient
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
5
|
+
# Common methods
|
6
|
+
# this is mixed in by other gems
|
7
|
+
module ClientMixin
|
8
|
+
ENTITY_METHODS = %w[get watch delete create update patch json_patch merge_patch apply].freeze
|
9
|
+
|
10
|
+
DEFAULT_SSL_OPTIONS = {
|
11
|
+
client_cert: nil,
|
12
|
+
client_key: nil,
|
13
|
+
ca_file: nil,
|
14
|
+
cert_store: nil,
|
15
|
+
verify_ssl: OpenSSL::SSL::VERIFY_PEER
|
16
|
+
}.freeze
|
17
|
+
|
18
|
+
DEFAULT_AUTH_OPTIONS = {
|
19
|
+
username: nil,
|
20
|
+
password: nil,
|
21
|
+
bearer_token: nil,
|
22
|
+
bearer_token_file: nil
|
23
|
+
}.freeze
|
24
|
+
|
25
|
+
DEFAULT_SOCKET_OPTIONS = {
|
26
|
+
socket_class: nil,
|
27
|
+
ssl_socket_class: nil
|
28
|
+
}.freeze
|
29
|
+
|
30
|
+
DEFAULT_TIMEOUTS = {
|
31
|
+
# These do NOT affect watch, watching never times out.
|
32
|
+
open: Net::HTTP.new('127.0.0.1').open_timeout, # depends on ruby version
|
33
|
+
read: Net::HTTP.new('127.0.0.1').read_timeout
|
34
|
+
}.freeze
|
35
|
+
|
36
|
+
DEFAULT_HTTP_PROXY_URI = nil
|
37
|
+
DEFAULT_HTTP_MAX_REDIRECTS = 10
|
38
|
+
|
39
|
+
SEARCH_ARGUMENTS = {
|
40
|
+
'labelSelector' => :label_selector,
|
41
|
+
'fieldSelector' => :field_selector,
|
42
|
+
'resourceVersion' => :resource_version,
|
43
|
+
'limit' => :limit,
|
44
|
+
'continue' => :continue
|
45
|
+
}.freeze
|
46
|
+
|
47
|
+
WATCH_ARGUMENTS = {
|
48
|
+
'labelSelector' => :label_selector,
|
49
|
+
'fieldSelector' => :field_selector,
|
50
|
+
'resourceVersion' => :resource_version
|
51
|
+
}.freeze
|
52
|
+
|
53
|
+
attr_reader :api_endpoint
|
54
|
+
attr_reader :ssl_options
|
55
|
+
attr_reader :auth_options
|
56
|
+
attr_reader :http_proxy_uri
|
57
|
+
attr_reader :http_max_redirects
|
58
|
+
attr_reader :headers
|
59
|
+
attr_reader :discovered
|
60
|
+
|
61
|
+
def initialize_client(
|
62
|
+
uri,
|
63
|
+
path,
|
64
|
+
version,
|
65
|
+
ssl_options: DEFAULT_SSL_OPTIONS,
|
66
|
+
auth_options: DEFAULT_AUTH_OPTIONS,
|
67
|
+
socket_options: DEFAULT_SOCKET_OPTIONS,
|
68
|
+
timeouts: DEFAULT_TIMEOUTS,
|
69
|
+
http_proxy_uri: DEFAULT_HTTP_PROXY_URI,
|
70
|
+
http_max_redirects: DEFAULT_HTTP_MAX_REDIRECTS,
|
71
|
+
as: :ros
|
72
|
+
)
|
73
|
+
validate_auth_options(auth_options)
|
74
|
+
handle_uri(uri, path)
|
75
|
+
|
76
|
+
@entities = {}
|
77
|
+
@discovered = false
|
78
|
+
@api_version = version
|
79
|
+
@headers = {}
|
80
|
+
@ssl_options = ssl_options
|
81
|
+
@auth_options = auth_options
|
82
|
+
@socket_options = socket_options
|
83
|
+
# Allow passing partial timeouts hash, without unspecified
|
84
|
+
# @timeouts[:foo] == nil resulting in infinite timeout.
|
85
|
+
@timeouts = DEFAULT_TIMEOUTS.merge(timeouts)
|
86
|
+
@http_proxy_uri = http_proxy_uri ? http_proxy_uri.to_s : nil
|
87
|
+
@http_max_redirects = http_max_redirects
|
88
|
+
@as = as
|
89
|
+
|
90
|
+
if auth_options[:bearer_token]
|
91
|
+
bearer_token(@auth_options[:bearer_token])
|
92
|
+
elsif auth_options[:bearer_token_file]
|
93
|
+
validate_bearer_token_file
|
94
|
+
bearer_token(File.read(@auth_options[:bearer_token_file]))
|
17
95
|
end
|
96
|
+
end
|
18
97
|
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
98
|
+
def method_missing(method_sym, *args, &block)
|
99
|
+
if discovery_needed?(method_sym)
|
100
|
+
discover
|
101
|
+
send(method_sym, *args, &block)
|
102
|
+
else
|
103
|
+
super
|
24
104
|
end
|
105
|
+
end
|
25
106
|
|
26
|
-
|
27
|
-
|
107
|
+
def respond_to_missing?(method_sym, include_private = false)
|
108
|
+
if discovery_needed?(method_sym)
|
109
|
+
discover
|
110
|
+
respond_to?(method_sym, include_private)
|
111
|
+
else
|
112
|
+
super
|
28
113
|
end
|
114
|
+
end
|
29
115
|
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
entity_types.each do |klass, entity_type|
|
34
|
-
entity_name = entity_type.underscore
|
35
|
-
entity_name_plural = entity_name.pluralize
|
36
|
-
|
37
|
-
# get all entities of a type e.g. get_nodes, get_pods, etc.
|
38
|
-
define_method("get_#{entity_name_plural}") do |options = {}|
|
39
|
-
get_entities(entity_type, klass, options)
|
40
|
-
end
|
116
|
+
def discovery_needed?(method_sym)
|
117
|
+
!@discovered && ENTITY_METHODS.any? { |x| method_sym.to_s.start_with?(x) }
|
118
|
+
end
|
41
119
|
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
120
|
+
def handle_exception
|
121
|
+
yield
|
122
|
+
rescue RestClient::Exception => e
|
123
|
+
json_error_msg = begin
|
124
|
+
JSON.parse(e.response || '') || {}
|
125
|
+
rescue JSON::ParserError
|
126
|
+
{}
|
127
|
+
end
|
128
|
+
err_message = json_error_msg['message'] || e.message
|
129
|
+
error_klass = e.http_code == 404 ? ResourceNotFoundError : HttpError
|
130
|
+
raise error_klass.new(e.http_code, err_message, e.response)
|
131
|
+
end
|
47
132
|
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
133
|
+
def discover
|
134
|
+
load_entities
|
135
|
+
define_entity_methods
|
136
|
+
@discovered = true
|
137
|
+
end
|
52
138
|
|
53
|
-
|
54
|
-
|
139
|
+
def self.parse_definition(kind, name)
|
140
|
+
# Kubernetes gives us 3 inputs:
|
141
|
+
# kind: "ComponentStatus", "NetworkPolicy", "Endpoints"
|
142
|
+
# name: "componentstatuses", "networkpolicies", "endpoints"
|
143
|
+
# singularName: "componentstatus" etc (usually omitted, defaults to kind.downcase)
|
144
|
+
# and want to derive singular and plural method names, with underscores:
|
145
|
+
# "network_policy"
|
146
|
+
# "network_policies"
|
147
|
+
# kind's CamelCase word boundaries determine our placement of underscores.
|
148
|
+
|
149
|
+
if IRREGULAR_NAMES[kind]
|
150
|
+
# In a few cases, the given kind / singularName itself is still plural.
|
151
|
+
# We require a distinct singular method name, so force it.
|
152
|
+
method_names = IRREGULAR_NAMES[kind]
|
153
|
+
else
|
154
|
+
# TODO: respect singularName from discovery?
|
155
|
+
# But how? If it differs from kind.downcase, kind's word boundaries don't apply.
|
156
|
+
singular_name = kind.downcase
|
157
|
+
|
158
|
+
if !(/[A-Z]/ =~ kind)
|
159
|
+
# Some custom resources have a fully lowercase kind - can't infer underscores.
|
160
|
+
method_names = [singular_name, name]
|
161
|
+
else
|
162
|
+
# Some plurals are not exact suffixes, e.g. NetworkPolicy -> networkpolicies.
|
163
|
+
# So don't expect full last word to match.
|
164
|
+
/^(?<prefix>(.*[A-Z]))(?<singular_suffix>[^A-Z]*)$/ =~ kind # "NetworkP", "olicy"
|
165
|
+
if name.start_with?(prefix.downcase)
|
166
|
+
plural_suffix = name[prefix.length..-1] # "olicies"
|
167
|
+
prefix_underscores = ClientMixin.underscore_entity(prefix) # "network_p"
|
168
|
+
method_names = [prefix_underscores + singular_suffix, # "network_policy"
|
169
|
+
prefix_underscores + plural_suffix] # "network_policies"
|
170
|
+
else
|
171
|
+
method_names = resolve_unconventional_method_names(name, kind, singular_name)
|
55
172
|
end
|
173
|
+
end
|
174
|
+
end
|
56
175
|
|
57
|
-
|
58
|
-
|
59
|
-
|
176
|
+
OpenStruct.new(
|
177
|
+
entity_type: kind,
|
178
|
+
resource_name: name,
|
179
|
+
method_names: method_names
|
180
|
+
)
|
181
|
+
end
|
60
182
|
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
183
|
+
def self.resolve_unconventional_method_names(name, kind, singular_name)
|
184
|
+
underscored_name = name.tr('-', '_')
|
185
|
+
singular_underscores = ClientMixin.underscore_entity(kind)
|
186
|
+
if underscored_name.start_with?(singular_underscores)
|
187
|
+
[singular_underscores, underscored_name]
|
188
|
+
else
|
189
|
+
# fallback to lowercase, no separators for both names
|
190
|
+
[singular_name, underscored_name.tr('_', '')]
|
65
191
|
end
|
192
|
+
end
|
66
193
|
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
194
|
+
def handle_uri(uri, path)
|
195
|
+
raise ArgumentError, 'Missing uri' unless uri
|
196
|
+
@api_endpoint = (uri.is_a?(URI) ? uri : URI.parse(uri))
|
197
|
+
|
198
|
+
# This regex will anchor at the last `/api`, `/oapi` or`/apis/:group`) part of the URL
|
199
|
+
# The whole path will be matched and if existing, the api_group will be extracted.
|
200
|
+
re = /^(?<path>.*\/o?api(?:s\/(?<apigroup>[^\/]+))?)$/mi
|
201
|
+
match = re.match(@api_endpoint.path.chomp('/'))
|
202
|
+
|
203
|
+
if match
|
204
|
+
# Since `re` captures 2 groups, match will always have 3 elements
|
205
|
+
# If thus we have a non-nil value in match 2, this is our api_group.
|
206
|
+
@api_group = match[:apigroup].nil? ? '' : match[:apigroup] + '/'
|
207
|
+
@api_endpoint.path = match[:path]
|
208
|
+
else
|
209
|
+
# This is a fallback, for when `/api` was not provided as part of the uri
|
210
|
+
@api_group = ''
|
211
|
+
@api_endpoint.path = @api_endpoint.path.chomp('/') + path
|
78
212
|
end
|
213
|
+
end
|
79
214
|
|
80
|
-
|
81
|
-
|
82
|
-
|
215
|
+
def build_namespace_prefix(namespace)
|
216
|
+
namespace.to_s.empty? ? '' : "namespaces/#{namespace}/"
|
217
|
+
end
|
218
|
+
|
219
|
+
# rubocop:disable Metrics/BlockLength
|
220
|
+
def define_entity_methods
|
221
|
+
@entities.values.each do |entity|
|
222
|
+
# get all entities of a type e.g. get_nodes, get_pods, etc.
|
223
|
+
define_singleton_method("get_#{entity.method_names[1]}") do |options = {}|
|
224
|
+
get_entities(entity.entity_type, entity.resource_name, options)
|
83
225
|
end
|
84
|
-
end
|
85
226
|
|
86
|
-
|
87
|
-
|
227
|
+
# watch all entities of a type e.g. watch_nodes, watch_pods, etc.
|
228
|
+
define_singleton_method("watch_#{entity.method_names[1]}") do |options = {}, &block|
|
229
|
+
# This method used to take resource_version as a param, so
|
230
|
+
# this conversion is to keep backwards compatibility
|
231
|
+
options = { resource_version: options } unless options.is_a?(Hash)
|
88
232
|
|
89
|
-
|
90
|
-
|
233
|
+
watch_entities(entity.resource_name, options, &block)
|
234
|
+
end
|
91
235
|
|
92
|
-
|
93
|
-
|
236
|
+
# get a single entity of a specific type by name
|
237
|
+
define_singleton_method("get_#{entity.method_names[0]}") \
|
238
|
+
do |name, namespace = nil, opts = {}|
|
239
|
+
get_entity(entity.resource_name, name, namespace, opts)
|
94
240
|
end
|
95
241
|
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
# http://ruby-doc.org/stdlib-1.9.3/libdoc/net/http/rdoc/Net/HTTP.html
|
101
|
-
verify_mode: @ssl_options[:verify_ssl],
|
102
|
-
cert: @ssl_options[:client_cert],
|
103
|
-
key: @ssl_options[:client_key],
|
104
|
-
basic_auth_user: @basic_auth_user,
|
105
|
-
basic_auth_password: @basic_auth_password,
|
106
|
-
headers: @headers
|
107
|
-
}
|
242
|
+
define_singleton_method("delete_#{entity.method_names[0]}") \
|
243
|
+
do |name, namespace = nil, opts = {}|
|
244
|
+
delete_entity(entity.resource_name, name, namespace, **opts)
|
245
|
+
end
|
108
246
|
|
109
|
-
|
110
|
-
|
247
|
+
define_singleton_method("create_#{entity.method_names[0]}") do |entity_config|
|
248
|
+
create_entity(entity.entity_type, entity.resource_name, entity_config)
|
249
|
+
end
|
111
250
|
|
112
|
-
|
113
|
-
|
114
|
-
if options[:label_selector]
|
115
|
-
params['params'] = { labelSelector: options[:label_selector] }
|
251
|
+
define_singleton_method("update_#{entity.method_names[0]}") do |entity_config|
|
252
|
+
update_entity(entity.resource_name, entity_config)
|
116
253
|
end
|
117
254
|
|
118
|
-
#
|
119
|
-
|
120
|
-
|
121
|
-
.get(params.merge(@headers))
|
255
|
+
define_singleton_method("patch_#{entity.method_names[0]}") \
|
256
|
+
do |name, patch, namespace = nil|
|
257
|
+
patch_entity(entity.resource_name, name, patch, 'strategic-merge-patch', namespace)
|
122
258
|
end
|
123
259
|
|
124
|
-
|
260
|
+
define_singleton_method("json_patch_#{entity.method_names[0]}") \
|
261
|
+
do |name, patch, namespace = nil|
|
262
|
+
patch_entity(entity.resource_name, name, patch, 'json-patch', namespace)
|
263
|
+
end
|
125
264
|
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
result.fetch('metadata', {}).fetch('resourceVersion', nil)
|
265
|
+
define_singleton_method("merge_patch_#{entity.method_names[0]}") \
|
266
|
+
do |name, patch, namespace = nil|
|
267
|
+
patch_entity(entity.resource_name, name, patch, 'merge-patch', namespace)
|
130
268
|
end
|
131
269
|
|
132
|
-
|
270
|
+
define_singleton_method("apply_#{entity.method_names[0]}") do |resource, opts = {}|
|
271
|
+
apply_entity(entity.resource_name, resource, **opts)
|
272
|
+
end
|
273
|
+
end
|
274
|
+
end
|
275
|
+
# rubocop:enable Metrics/BlockLength
|
133
276
|
|
134
|
-
|
277
|
+
def self.underscore_entity(entity_name)
|
278
|
+
entity_name.gsub(/([a-z])([A-Z])/, '\1_\2').downcase
|
279
|
+
end
|
280
|
+
|
281
|
+
def create_rest_client(path = nil)
|
282
|
+
path ||= @api_endpoint.path
|
283
|
+
options = {
|
284
|
+
ssl_ca_file: @ssl_options[:ca_file],
|
285
|
+
ssl_cert_store: @ssl_options[:cert_store],
|
286
|
+
verify_ssl: @ssl_options[:verify_ssl],
|
287
|
+
ssl_client_cert: @ssl_options[:client_cert],
|
288
|
+
ssl_client_key: @ssl_options[:client_key],
|
289
|
+
proxy: @http_proxy_uri,
|
290
|
+
max_redirects: @http_max_redirects,
|
291
|
+
user: @auth_options[:username],
|
292
|
+
password: @auth_options[:password],
|
293
|
+
open_timeout: @timeouts[:open],
|
294
|
+
read_timeout: @timeouts[:read]
|
295
|
+
}
|
296
|
+
RestClient::Resource.new(@api_endpoint.merge(path).to_s, options)
|
297
|
+
end
|
298
|
+
|
299
|
+
def rest_client
|
300
|
+
@rest_client ||= begin
|
301
|
+
create_rest_client("#{@api_endpoint.path}/#{@api_version}")
|
135
302
|
end
|
303
|
+
end
|
304
|
+
|
305
|
+
# Accepts the following options:
|
306
|
+
# :namespace (string) - the namespace of the entity.
|
307
|
+
# :name (string) - the name of the entity to watch.
|
308
|
+
# :label_selector (string) - a selector to restrict the list of returned objects by labels.
|
309
|
+
# :field_selector (string) - a selector to restrict the list of returned objects by fields.
|
310
|
+
# :resource_version (string) - shows changes that occur after passed version of a resource.
|
311
|
+
# :as (:raw|:ros) - defaults to :ros
|
312
|
+
# :raw - return the raw response body as a string
|
313
|
+
# :ros - return a collection of RecursiveOpenStruct objects
|
314
|
+
# Accepts an optional block, that will be called with each entity,
|
315
|
+
# otherwise returns a WatchStream
|
316
|
+
def watch_entities(resource_name, options = {}, &block)
|
317
|
+
ns = build_namespace_prefix(options[:namespace])
|
318
|
+
|
319
|
+
path = "watch/#{ns}#{resource_name}"
|
320
|
+
path += "/#{options[:name]}" if options[:name]
|
321
|
+
uri = @api_endpoint.merge("#{@api_endpoint.path}/#{@api_version}/#{path}")
|
322
|
+
|
323
|
+
params = {}
|
324
|
+
WATCH_ARGUMENTS.each { |k, v| params[k] = options[v] if options[v] }
|
325
|
+
uri.query = URI.encode_www_form(params) if params.any?
|
326
|
+
|
327
|
+
watcher = Kubeclient::Common::WatchStream.new(
|
328
|
+
uri,
|
329
|
+
http_options(uri),
|
330
|
+
formatter: ->(value) { format_response(options[:as] || @as, value) }
|
331
|
+
)
|
332
|
+
|
333
|
+
return_or_yield_to_watcher(watcher, &block)
|
334
|
+
end
|
136
335
|
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
336
|
+
# Accepts the following options:
|
337
|
+
# :namespace (string) - the namespace of the entity.
|
338
|
+
# :label_selector (string) - a selector to restrict the list of returned objects by labels.
|
339
|
+
# :field_selector (string) - a selector to restrict the list of returned objects by fields.
|
340
|
+
# :limit (integer) - a maximum number of items to return in each response
|
341
|
+
# :continue (string) - a token used to retrieve the next chunk of entities
|
342
|
+
# :as (:raw|:ros) - defaults to :ros
|
343
|
+
# :raw - return the raw response body as a string
|
344
|
+
# :ros - return a collection of RecursiveOpenStruct objects
|
345
|
+
def get_entities(entity_type, resource_name, options = {})
|
346
|
+
params = {}
|
347
|
+
SEARCH_ARGUMENTS.each { |k, v| params[k] = options[v] if options[v] }
|
348
|
+
|
349
|
+
ns_prefix = build_namespace_prefix(options[:namespace])
|
350
|
+
response = handle_exception do
|
351
|
+
rest_client[ns_prefix + resource_name]
|
352
|
+
.get({ 'params' => params }.merge(@headers))
|
353
|
+
end
|
354
|
+
format_response(options[:as] || @as, response.body, entity_type)
|
355
|
+
end
|
356
|
+
|
357
|
+
# Accepts the following options:
|
358
|
+
# :as (:raw|:ros) - defaults to :ros
|
359
|
+
# :raw - return the raw response body as a string
|
360
|
+
# :ros - return a collection of RecursiveOpenStruct objects
|
361
|
+
def get_entity(resource_name, name, namespace = nil, options = {})
|
362
|
+
ns_prefix = build_namespace_prefix(namespace)
|
363
|
+
response = handle_exception do
|
364
|
+
rest_client[ns_prefix + resource_name + "/#{name}"]
|
141
365
|
.get(@headers)
|
142
|
-
end
|
143
|
-
result = JSON.parse(response)
|
144
|
-
new_entity(result, klass)
|
145
366
|
end
|
367
|
+
format_response(options[:as] || @as, response.body)
|
368
|
+
end
|
146
369
|
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
370
|
+
# delete_options are passed as a JSON payload in the delete request
|
371
|
+
def delete_entity(resource_name, name, namespace = nil, delete_options: {})
|
372
|
+
delete_options_hash = delete_options.to_hash
|
373
|
+
ns_prefix = build_namespace_prefix(namespace)
|
374
|
+
payload = delete_options_hash.to_json unless delete_options_hash.empty?
|
375
|
+
response = handle_exception do
|
376
|
+
rs = rest_client[ns_prefix + resource_name + "/#{name}"]
|
377
|
+
RestClient::Request.execute(
|
378
|
+
rs.options.merge(
|
379
|
+
method: :delete,
|
380
|
+
url: rs.url,
|
381
|
+
headers: { 'Content-Type' => 'application/json' }.merge(@headers),
|
382
|
+
payload: payload
|
383
|
+
)
|
384
|
+
)
|
153
385
|
end
|
386
|
+
format_response(@as, response.body)
|
387
|
+
end
|
154
388
|
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
389
|
+
def create_entity(entity_type, resource_name, entity_config)
|
390
|
+
# Duplicate the entity_config to a hash so that when we assign
|
391
|
+
# kind and apiVersion, this does not mutate original entity_config obj.
|
392
|
+
hash = entity_config.to_hash
|
393
|
+
|
394
|
+
ns_prefix = build_namespace_prefix(hash[:metadata][:namespace])
|
395
|
+
|
396
|
+
# TODO: temporary solution to add "kind" and apiVersion to request
|
397
|
+
# until this issue is solved
|
398
|
+
# https://github.com/GoogleCloudPlatform/kubernetes/issues/6439
|
399
|
+
hash[:kind] = entity_type
|
400
|
+
hash[:apiVersion] = @api_group + @api_version
|
401
|
+
response = handle_exception do
|
402
|
+
rest_client[ns_prefix + resource_name]
|
403
|
+
.post(hash.to_json, { 'Content-Type' => 'application/json' }.merge(@headers))
|
404
|
+
end
|
405
|
+
format_response(@as, response.body)
|
406
|
+
end
|
159
407
|
|
160
|
-
|
408
|
+
def update_entity(resource_name, entity_config)
|
409
|
+
name = entity_config[:metadata][:name]
|
410
|
+
ns_prefix = build_namespace_prefix(entity_config[:metadata][:namespace])
|
411
|
+
response = handle_exception do
|
412
|
+
rest_client[ns_prefix + resource_name + "/#{name}"]
|
413
|
+
.put(entity_config.to_h.to_json, { 'Content-Type' => 'application/json' }.merge(@headers))
|
414
|
+
end
|
415
|
+
format_response(@as, response.body)
|
416
|
+
end
|
161
417
|
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
418
|
+
def patch_entity(resource_name, name, patch, strategy, namespace)
|
419
|
+
ns_prefix = build_namespace_prefix(namespace)
|
420
|
+
response = handle_exception do
|
421
|
+
rest_client[ns_prefix + resource_name + "/#{name}"]
|
422
|
+
.patch(
|
423
|
+
patch.to_json,
|
424
|
+
{ 'Content-Type' => "application/#{strategy}+json" }.merge(@headers)
|
425
|
+
)
|
426
|
+
end
|
427
|
+
format_response(@as, response.body)
|
428
|
+
end
|
429
|
+
|
430
|
+
def apply_entity(resource_name, resource, field_manager:, force: true)
|
431
|
+
name = "#{resource[:metadata][:name]}?fieldManager=#{field_manager}&force=#{force}"
|
432
|
+
ns_prefix = build_namespace_prefix(resource[:metadata][:namespace])
|
433
|
+
response = handle_exception do
|
434
|
+
rest_client[ns_prefix + resource_name + "/#{name}"]
|
435
|
+
.patch(
|
436
|
+
resource.to_json,
|
437
|
+
{ 'Content-Type' => 'application/apply-patch+yaml' }.merge(@headers)
|
438
|
+
)
|
439
|
+
end
|
440
|
+
format_response(@as, response.body)
|
441
|
+
end
|
442
|
+
|
443
|
+
def all_entities(options = {})
|
444
|
+
discover unless @discovered
|
445
|
+
@entities.values.each_with_object({}) do |entity, result_hash|
|
446
|
+
# method call for get each entities
|
447
|
+
# build hash of entity name to array of the entities
|
448
|
+
method_name = "get_#{entity.method_names[1]}"
|
449
|
+
begin
|
450
|
+
result_hash[entity.method_names[0]] = send(method_name, options)
|
451
|
+
rescue Kubeclient::HttpError
|
452
|
+
next # do not fail due to resources not supporting get
|
184
453
|
end
|
185
454
|
end
|
455
|
+
end
|
186
456
|
|
187
|
-
|
188
|
-
|
457
|
+
def get_pod_log(pod_name, namespace,
|
458
|
+
container: nil, previous: false,
|
459
|
+
timestamps: false, since_time: nil, tail_lines: nil, limit_bytes: nil)
|
460
|
+
params = {}
|
461
|
+
params[:previous] = true if previous
|
462
|
+
params[:container] = container if container
|
463
|
+
params[:timestamps] = timestamps if timestamps
|
464
|
+
params[:sinceTime] = format_datetime(since_time) if since_time
|
465
|
+
params[:tailLines] = tail_lines if tail_lines
|
466
|
+
params[:limitBytes] = limit_bytes if limit_bytes
|
467
|
+
|
468
|
+
ns = build_namespace_prefix(namespace)
|
469
|
+
handle_exception do
|
470
|
+
rest_client[ns + "pods/#{pod_name}/log"]
|
471
|
+
.get({ 'params' => params }.merge(@headers))
|
189
472
|
end
|
473
|
+
end
|
474
|
+
|
475
|
+
def watch_pod_log(pod_name, namespace, container: nil, &block)
|
476
|
+
# Adding the "follow=true" query param tells the Kubernetes API to keep
|
477
|
+
# the connection open and stream updates to the log.
|
478
|
+
params = { follow: true }
|
479
|
+
params[:container] = container if container
|
480
|
+
|
481
|
+
ns = build_namespace_prefix(namespace)
|
482
|
+
|
483
|
+
uri = @api_endpoint.dup
|
484
|
+
uri.path += "/#{@api_version}/#{ns}pods/#{pod_name}/log"
|
485
|
+
uri.query = URI.encode_www_form(params)
|
190
486
|
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
487
|
+
watcher = Kubeclient::Common::WatchStream.new(
|
488
|
+
uri, http_options(uri), formatter: ->(value) { value }
|
489
|
+
)
|
490
|
+
return_or_yield_to_watcher(watcher, &block)
|
491
|
+
end
|
492
|
+
|
493
|
+
def proxy_url(kind, name, port, namespace = '')
|
494
|
+
discover unless @discovered
|
495
|
+
entity_name_plural =
|
496
|
+
if %w[services pods nodes].include?(kind.to_s)
|
497
|
+
kind.to_s
|
498
|
+
else
|
499
|
+
@entities[kind.to_s].resource_name
|
198
500
|
end
|
501
|
+
ns_prefix = build_namespace_prefix(namespace)
|
502
|
+
rest_client["#{ns_prefix}#{entity_name_plural}/#{name}:#{port}/proxy"].url
|
503
|
+
end
|
504
|
+
|
505
|
+
def process_template(template)
|
506
|
+
ns_prefix = build_namespace_prefix(template[:metadata][:namespace])
|
507
|
+
response = handle_exception do
|
508
|
+
rest_client[ns_prefix + 'processedtemplates']
|
509
|
+
.post(template.to_h.to_json, { 'Content-Type' => 'application/json' }.merge(@headers))
|
199
510
|
end
|
511
|
+
JSON.parse(response)
|
512
|
+
end
|
200
513
|
|
201
|
-
|
202
|
-
|
514
|
+
def api_valid?
|
515
|
+
result = api
|
516
|
+
result.is_a?(Hash) && (result['versions'] || []).any? do |group|
|
517
|
+
@api_group.empty? ? group.include?(@api_version) : group['version'] == @api_version
|
203
518
|
end
|
519
|
+
end
|
204
520
|
|
205
|
-
|
206
|
-
|
207
|
-
|
521
|
+
def api
|
522
|
+
response = handle_exception { create_rest_client.get(@headers) }
|
523
|
+
JSON.parse(response)
|
524
|
+
end
|
525
|
+
|
526
|
+
private
|
527
|
+
|
528
|
+
IRREGULAR_NAMES = {
|
529
|
+
# In a few cases, the given kind itself is still plural.
|
530
|
+
# https://github.com/kubernetes/kubernetes/issues/8115
|
531
|
+
'Endpoints' => %w[endpoint endpoints],
|
532
|
+
'SecurityContextConstraints' => %w[security_context_constraint
|
533
|
+
security_context_constraints]
|
534
|
+
}.freeze
|
535
|
+
|
536
|
+
# Format datetime according to RFC3339
|
537
|
+
def format_datetime(value)
|
538
|
+
case value
|
539
|
+
when DateTime, Time
|
540
|
+
value.strftime('%FT%T.%9N%:z')
|
541
|
+
when String
|
542
|
+
value
|
543
|
+
else
|
544
|
+
raise ArgumentError, "unsupported type '#{value.class}' of time value '#{value}'"
|
208
545
|
end
|
546
|
+
end
|
209
547
|
|
210
|
-
|
211
|
-
|
212
|
-
|
548
|
+
def format_response(as, body, list_type = nil)
|
549
|
+
case as
|
550
|
+
when :raw
|
551
|
+
body
|
552
|
+
when :parsed
|
553
|
+
JSON.parse(body)
|
554
|
+
when :parsed_symbolized
|
555
|
+
JSON.parse(body, symbolize_names: true)
|
556
|
+
when :ros
|
557
|
+
result = JSON.parse(body)
|
558
|
+
|
559
|
+
if list_type
|
560
|
+
resource_version =
|
561
|
+
result.fetch('resourceVersion') do
|
562
|
+
result.fetch('metadata', {}).fetch('resourceVersion', nil)
|
563
|
+
end
|
564
|
+
|
565
|
+
# If 'limit' was passed save the continue token
|
566
|
+
# see https://kubernetes.io/docs/reference/using-api/api-concepts/#retrieving-large-results-sets-in-chunks
|
567
|
+
continue = result.fetch('metadata', {}).fetch('continue', nil)
|
568
|
+
|
569
|
+
# result['items'] might be nil due to https://github.com/kubernetes/kubernetes/issues/13096
|
570
|
+
collection = result['items'].to_a.map { |item| Kubeclient::Resource.new(item) }
|
571
|
+
|
572
|
+
Kubeclient::Common::EntityList.new(list_type, resource_version, collection, continue)
|
573
|
+
else
|
574
|
+
Kubeclient::Resource.new(result)
|
213
575
|
end
|
214
|
-
|
576
|
+
else
|
577
|
+
raise ArgumentError, "Unsupported format #{as.inspect}"
|
578
|
+
end
|
579
|
+
end
|
580
|
+
|
581
|
+
def load_entities
|
582
|
+
@entities = {}
|
583
|
+
fetch_entities['resources'].each do |resource|
|
584
|
+
next if resource['name'].include?('/')
|
585
|
+
# Not a regular entity, special functionality covered by `process_template`.
|
586
|
+
# https://github.com/openshift/origin/issues/21668
|
587
|
+
next if resource['kind'] == 'Template' && resource['name'] == 'processedtemplates'
|
588
|
+
resource['kind'] ||=
|
589
|
+
Kubeclient::Common::MissingKindCompatibility.resource_kind(resource['name'])
|
590
|
+
entity = ClientMixin.parse_definition(resource['kind'], resource['name'])
|
591
|
+
@entities[entity.method_names[0]] = entity if entity
|
215
592
|
end
|
593
|
+
end
|
216
594
|
|
217
|
-
|
595
|
+
def fetch_entities
|
596
|
+
JSON.parse(handle_exception { rest_client.get(@headers) })
|
597
|
+
end
|
218
598
|
|
219
|
-
|
220
|
-
|
221
|
-
|
599
|
+
def bearer_token(bearer_token)
|
600
|
+
@headers ||= {}
|
601
|
+
@headers[:Authorization] = "Bearer #{bearer_token}"
|
602
|
+
end
|
603
|
+
|
604
|
+
def validate_auth_options(opts)
|
605
|
+
# maintain backward compatibility:
|
606
|
+
opts[:username] = opts[:user] if opts[:user]
|
607
|
+
|
608
|
+
if %i[bearer_token bearer_token_file username].count { |key| opts[key] } > 1
|
609
|
+
raise(
|
610
|
+
ArgumentError,
|
611
|
+
'Invalid auth options: specify only one of username/password,' \
|
612
|
+
' bearer_token or bearer_token_file'
|
613
|
+
)
|
614
|
+
elsif %i[username password].count { |key| opts[key] } == 1
|
615
|
+
raise ArgumentError, 'Basic auth requires both username & password'
|
222
616
|
end
|
223
617
|
end
|
618
|
+
|
619
|
+
def validate_bearer_token_file
|
620
|
+
msg = "Token file #{@auth_options[:bearer_token_file]} does not exist"
|
621
|
+
raise ArgumentError, msg unless File.file?(@auth_options[:bearer_token_file])
|
622
|
+
|
623
|
+
msg = "Cannot read token file #{@auth_options[:bearer_token_file]}"
|
624
|
+
raise ArgumentError, msg unless File.readable?(@auth_options[:bearer_token_file])
|
625
|
+
end
|
626
|
+
|
627
|
+
def return_or_yield_to_watcher(watcher, &block)
|
628
|
+
return watcher unless block_given?
|
629
|
+
|
630
|
+
begin
|
631
|
+
watcher.each(&block)
|
632
|
+
ensure
|
633
|
+
watcher.finish
|
634
|
+
end
|
635
|
+
end
|
636
|
+
|
637
|
+
def http_options(uri)
|
638
|
+
options = {
|
639
|
+
basic_auth_user: @auth_options[:username],
|
640
|
+
basic_auth_password: @auth_options[:password],
|
641
|
+
headers: @headers,
|
642
|
+
http_proxy_uri: @http_proxy_uri,
|
643
|
+
http_max_redirects: http_max_redirects
|
644
|
+
}
|
645
|
+
|
646
|
+
if uri.scheme == 'https'
|
647
|
+
options[:ssl] = {
|
648
|
+
ca_file: @ssl_options[:ca_file],
|
649
|
+
cert: @ssl_options[:client_cert],
|
650
|
+
cert_store: @ssl_options[:cert_store],
|
651
|
+
key: @ssl_options[:client_key],
|
652
|
+
# ruby HTTP uses verify_mode instead of verify_ssl
|
653
|
+
# http://ruby-doc.org/stdlib-1.9.3/libdoc/openssl/rdoc/OpenSSL/SSL/SSLContext.html
|
654
|
+
verify_mode: @ssl_options[:verify_ssl]
|
655
|
+
}
|
656
|
+
end
|
657
|
+
|
658
|
+
options.merge(@socket_options)
|
659
|
+
end
|
224
660
|
end
|
225
661
|
end
|