kubeclient-rollback-dev 2.3.0
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 +7 -0
- data/.gitignore +15 -0
- data/.rubocop.yml +16 -0
- data/.travis.yml +12 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +428 -0
- data/Rakefile +12 -0
- data/kubeclient.gemspec +29 -0
- data/lib/kubeclient/common.rb +512 -0
- data/lib/kubeclient/config.rb +126 -0
- data/lib/kubeclient/entity_list.rb +16 -0
- data/lib/kubeclient/kube_exception.rb +14 -0
- data/lib/kubeclient/missing_kind_compatibility.rb +68 -0
- data/lib/kubeclient/version.rb +4 -0
- data/lib/kubeclient/watch_notice.rb +7 -0
- data/lib/kubeclient/watch_stream.rb +80 -0
- data/lib/kubeclient.rb +32 -0
- data/test/cassettes/kubernetes_guestbook.yml +879 -0
- data/test/config/allinone.kubeconfig +20 -0
- data/test/config/external-ca.pem +18 -0
- data/test/config/external-cert.pem +19 -0
- data/test/config/external-key.rsa +27 -0
- data/test/config/external.kubeconfig +20 -0
- data/test/config/nouser.kubeconfig +16 -0
- data/test/config/userauth.kubeconfig +28 -0
- data/test/json/bindings_list.json +10 -0
- data/test/json/component_status.json +17 -0
- data/test/json/component_status_list.json +52 -0
- data/test/json/config_map_list.json +9 -0
- data/test/json/core_api_resource_list.json +181 -0
- data/test/json/core_api_resource_list_without_kind.json +129 -0
- data/test/json/core_oapi_resource_list_without_kind.json +197 -0
- data/test/json/created_endpoint.json +28 -0
- data/test/json/created_namespace.json +20 -0
- data/test/json/created_secret.json +16 -0
- data/test/json/created_service.json +31 -0
- data/test/json/empty_pod_list.json +9 -0
- data/test/json/endpoint_list.json +48 -0
- data/test/json/entity_list.json +56 -0
- data/test/json/event_list.json +35 -0
- data/test/json/limit_range.json +23 -0
- data/test/json/limit_range_list.json +31 -0
- data/test/json/namespace.json +13 -0
- data/test/json/namespace_exception.json +8 -0
- data/test/json/namespace_list.json +32 -0
- data/test/json/node.json +29 -0
- data/test/json/node_list.json +37 -0
- data/test/json/persistent_volume.json +37 -0
- data/test/json/persistent_volume_claim.json +32 -0
- data/test/json/persistent_volume_claim_list.json +40 -0
- data/test/json/persistent_volume_claims_nil_items.json +8 -0
- data/test/json/persistent_volume_list.json +45 -0
- data/test/json/pod.json +92 -0
- data/test/json/pod_list.json +79 -0
- data/test/json/pod_template_list.json +9 -0
- data/test/json/processed_template.json +27 -0
- data/test/json/replication_controller.json +57 -0
- data/test/json/replication_controller_list.json +66 -0
- data/test/json/resource_quota.json +46 -0
- data/test/json/resource_quota_list.json +54 -0
- data/test/json/secret_list.json +44 -0
- data/test/json/service.json +33 -0
- data/test/json/service_account.json +25 -0
- data/test/json/service_account_list.json +82 -0
- data/test/json/service_illegal_json_404.json +1 -0
- data/test/json/service_list.json +97 -0
- data/test/json/service_patch.json +25 -0
- data/test/json/service_update.json +22 -0
- data/test/json/versions_list.json +6 -0
- data/test/json/watch_stream.json +3 -0
- data/test/test_common.rb +32 -0
- data/test/test_component_status.rb +30 -0
- data/test/test_config.rb +72 -0
- data/test/test_endpoint.rb +35 -0
- data/test/test_guestbook_go.rb +238 -0
- data/test/test_helper.rb +10 -0
- data/test/test_kubeclient.rb +611 -0
- data/test/test_limit_range.rb +27 -0
- data/test/test_missing_methods.rb +42 -0
- data/test/test_namespace.rb +61 -0
- data/test/test_node.rb +33 -0
- data/test/test_persistent_volume.rb +30 -0
- data/test/test_persistent_volume_claim.rb +30 -0
- data/test/test_pod.rb +29 -0
- data/test/test_pod_log.rb +50 -0
- data/test/test_process_template.rb +44 -0
- data/test/test_replication_controller.rb +27 -0
- data/test/test_resource_list_without_kind.rb +78 -0
- data/test/test_resource_quota.rb +25 -0
- data/test/test_secret.rb +70 -0
- data/test/test_service.rb +293 -0
- data/test/test_service_account.rb +28 -0
- data/test/test_watch.rb +119 -0
- data/test/txt/pod_log.txt +6 -0
- data/test/valid_token_file +1 -0
- metadata +343 -0
@@ -0,0 +1,512 @@
|
|
1
|
+
require 'json'
|
2
|
+
require 'rest-client'
|
3
|
+
module Kubeclient
|
4
|
+
# Common methods
|
5
|
+
# this is mixed in by other gems
|
6
|
+
module ClientMixin
|
7
|
+
ENTITY_METHODS = %w(get watch delete create update patch rollback).freeze
|
8
|
+
|
9
|
+
DEFAULT_SSL_OPTIONS = {
|
10
|
+
client_cert: nil,
|
11
|
+
client_key: nil,
|
12
|
+
ca_file: nil,
|
13
|
+
cert_store: nil,
|
14
|
+
verify_ssl: OpenSSL::SSL::VERIFY_PEER
|
15
|
+
}.freeze
|
16
|
+
|
17
|
+
DEFAULT_AUTH_OPTIONS = {
|
18
|
+
username: nil,
|
19
|
+
password: nil,
|
20
|
+
bearer_token: nil,
|
21
|
+
bearer_token_file: nil
|
22
|
+
}.freeze
|
23
|
+
|
24
|
+
DEFAULT_SOCKET_OPTIONS = {
|
25
|
+
socket_class: nil,
|
26
|
+
ssl_socket_class: nil
|
27
|
+
}.freeze
|
28
|
+
|
29
|
+
DEFAULT_HTTP_PROXY_URI = nil
|
30
|
+
|
31
|
+
SEARCH_ARGUMENTS = {
|
32
|
+
'labelSelector' => :label_selector,
|
33
|
+
'fieldSelector' => :field_selector
|
34
|
+
}.freeze
|
35
|
+
|
36
|
+
WATCH_ARGUMENTS = { 'resourceVersion' => :resource_version }.merge!(SEARCH_ARGUMENTS).freeze
|
37
|
+
|
38
|
+
attr_reader :api_endpoint
|
39
|
+
attr_reader :ssl_options
|
40
|
+
attr_reader :auth_options
|
41
|
+
attr_reader :http_proxy_uri
|
42
|
+
attr_reader :headers
|
43
|
+
attr_reader :discovered
|
44
|
+
|
45
|
+
def initialize_client(
|
46
|
+
class_owner,
|
47
|
+
uri,
|
48
|
+
path,
|
49
|
+
version,
|
50
|
+
ssl_options: DEFAULT_SSL_OPTIONS,
|
51
|
+
auth_options: DEFAULT_AUTH_OPTIONS,
|
52
|
+
socket_options: DEFAULT_SOCKET_OPTIONS,
|
53
|
+
http_proxy_uri: DEFAULT_HTTP_PROXY_URI
|
54
|
+
)
|
55
|
+
validate_auth_options(auth_options)
|
56
|
+
handle_uri(uri, path)
|
57
|
+
|
58
|
+
@class_owner = class_owner
|
59
|
+
@entities = {}
|
60
|
+
@discovered = false
|
61
|
+
@api_version = version
|
62
|
+
@headers = {}
|
63
|
+
@ssl_options = ssl_options
|
64
|
+
@auth_options = auth_options
|
65
|
+
@socket_options = socket_options
|
66
|
+
@http_proxy_uri = http_proxy_uri.to_s if http_proxy_uri
|
67
|
+
|
68
|
+
if auth_options[:bearer_token]
|
69
|
+
bearer_token(@auth_options[:bearer_token])
|
70
|
+
elsif auth_options[:bearer_token_file]
|
71
|
+
validate_bearer_token_file
|
72
|
+
bearer_token(File.read(@auth_options[:bearer_token_file]))
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def method_missing(method_sym, *args, &block)
|
77
|
+
if discovery_needed?(method_sym)
|
78
|
+
discover
|
79
|
+
send(method_sym, *args, &block)
|
80
|
+
else
|
81
|
+
super
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def respond_to_missing?(method_sym, include_private = false)
|
86
|
+
if discovery_needed?(method_sym)
|
87
|
+
discover
|
88
|
+
respond_to?(method_sym, include_private)
|
89
|
+
else
|
90
|
+
super
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
def discovery_needed?(method_sym)
|
95
|
+
!@discovered && ENTITY_METHODS.any? { |x| method_sym.to_s.start_with?(x) }
|
96
|
+
end
|
97
|
+
|
98
|
+
def handle_exception
|
99
|
+
yield
|
100
|
+
rescue RestClient::Exception => e
|
101
|
+
json_error_msg = begin
|
102
|
+
JSON.parse(e.response || '') || {}
|
103
|
+
rescue JSON::ParserError
|
104
|
+
{}
|
105
|
+
end
|
106
|
+
err_message = json_error_msg['message'] || e.message
|
107
|
+
raise KubeException.new(e.http_code, err_message, e.response)
|
108
|
+
end
|
109
|
+
|
110
|
+
def discover
|
111
|
+
load_entities
|
112
|
+
define_entity_methods
|
113
|
+
@discovered = true
|
114
|
+
end
|
115
|
+
|
116
|
+
def self.parse_definition(kind, name)
|
117
|
+
# "name": "componentstatuses", networkpolicies, endpoints
|
118
|
+
# "kind": "ComponentStatus" NetworkPolicy, Endpoints
|
119
|
+
# maintain pre group api compatibility for endpoints.
|
120
|
+
# See: https://github.com/kubernetes/kubernetes/issues/8115
|
121
|
+
kind = 'Endpoint' if kind == 'Endpoints'
|
122
|
+
|
123
|
+
prefix = kind[0..kind.rindex(/[A-Z]/)] # NetworkP
|
124
|
+
m = name.match(/^#{prefix.downcase}(.*)$/)
|
125
|
+
m && OpenStruct.new(
|
126
|
+
entity_type: kind, # ComponentStatus
|
127
|
+
resource_name: name, # componentstatuses
|
128
|
+
method_names: [
|
129
|
+
ClientMixin.underscore_entity(kind), # component_status
|
130
|
+
ClientMixin.underscore_entity(prefix) + m[1] # component_statuses
|
131
|
+
]
|
132
|
+
)
|
133
|
+
end
|
134
|
+
|
135
|
+
def handle_uri(uri, path)
|
136
|
+
raise ArgumentError, 'Missing uri' unless uri
|
137
|
+
@api_endpoint = (uri.is_a?(URI) ? uri : URI.parse(uri))
|
138
|
+
@api_endpoint.path = path if @api_endpoint.path.empty?
|
139
|
+
@api_endpoint.path = @api_endpoint.path.chop if @api_endpoint.path.end_with? '/'
|
140
|
+
components = @api_endpoint.path.to_s.split('/') # ["", "api"] or ["", "apis", batch]
|
141
|
+
@api_group = components.length > 2 ? components[2] + '/' : ''
|
142
|
+
end
|
143
|
+
|
144
|
+
def build_namespace_prefix(namespace)
|
145
|
+
namespace.to_s.empty? ? '' : "namespaces/#{namespace}/"
|
146
|
+
end
|
147
|
+
|
148
|
+
def self.resource_class(class_owner, entity_type)
|
149
|
+
if class_owner.const_defined?(entity_type, false)
|
150
|
+
class_owner.const_get(entity_type, false)
|
151
|
+
else
|
152
|
+
class_owner.const_set(
|
153
|
+
entity_type,
|
154
|
+
Class.new(RecursiveOpenStruct) do
|
155
|
+
def initialize(hash = nil, args = {})
|
156
|
+
args[:recurse_over_arrays] = true
|
157
|
+
super(hash, args)
|
158
|
+
end
|
159
|
+
end
|
160
|
+
)
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
def define_entity_methods
|
165
|
+
@entities.values.each do |entity|
|
166
|
+
klass = ClientMixin.resource_class(@class_owner, entity.entity_type)
|
167
|
+
# get all entities of a type e.g. get_nodes, get_pods, etc.
|
168
|
+
define_singleton_method("get_#{entity.method_names[1]}") do |options = {}|
|
169
|
+
get_entities(entity.entity_type, klass, entity.resource_name, options)
|
170
|
+
end
|
171
|
+
|
172
|
+
# watch all entities of a type e.g. watch_nodes, watch_pods, etc.
|
173
|
+
define_singleton_method("watch_#{entity.method_names[1]}") do |options = {}|
|
174
|
+
# This method used to take resource_version as a param, so
|
175
|
+
# this conversion is to keep backwards compatibility
|
176
|
+
options = { resource_version: options } unless options.is_a?(Hash)
|
177
|
+
|
178
|
+
watch_entities(entity.resource_name, options)
|
179
|
+
end
|
180
|
+
|
181
|
+
# get a single entity of a specific type by name
|
182
|
+
define_singleton_method("get_#{entity.method_names[0]}") do |name, namespace = nil|
|
183
|
+
get_entity(klass, entity.resource_name, name, namespace)
|
184
|
+
end
|
185
|
+
|
186
|
+
define_singleton_method("delete_#{entity.method_names[0]}") do |name, namespace = nil|
|
187
|
+
delete_entity(entity.resource_name, name, namespace)
|
188
|
+
end
|
189
|
+
|
190
|
+
define_singleton_method("create_#{entity.method_names[0]}") do |entity_config|
|
191
|
+
create_entity(entity.entity_type, entity.resource_name, entity_config, klass)
|
192
|
+
end
|
193
|
+
|
194
|
+
define_singleton_method("update_#{entity.method_names[0]}") do |entity_config|
|
195
|
+
update_entity(entity.resource_name, entity_config)
|
196
|
+
end
|
197
|
+
|
198
|
+
define_singleton_method("patch_#{entity.method_names[0]}") do |name, patch, namespace = nil|
|
199
|
+
patch_entity(entity.resource_name, name, patch, namespace)
|
200
|
+
end
|
201
|
+
|
202
|
+
define_singleton_method("rollback_#{entity.method_names[0]}") do |name, entity_config = {}, namespace = nil|
|
203
|
+
rollback_entity(entity.resource_name, name, entity_config: entity_config, namespace: namespace)
|
204
|
+
end
|
205
|
+
end
|
206
|
+
end
|
207
|
+
|
208
|
+
def self.underscore_entity(entity_name)
|
209
|
+
entity_name.gsub(/([a-z])([A-Z])/, '\1_\2').downcase
|
210
|
+
end
|
211
|
+
|
212
|
+
def create_rest_client(path = nil)
|
213
|
+
path ||= @api_endpoint.path
|
214
|
+
options = {
|
215
|
+
ssl_ca_file: @ssl_options[:ca_file],
|
216
|
+
ssl_cert_store: @ssl_options[:cert_store],
|
217
|
+
verify_ssl: @ssl_options[:verify_ssl],
|
218
|
+
ssl_client_cert: @ssl_options[:client_cert],
|
219
|
+
ssl_client_key: @ssl_options[:client_key],
|
220
|
+
proxy: @http_proxy_uri,
|
221
|
+
user: @auth_options[:username],
|
222
|
+
password: @auth_options[:password]
|
223
|
+
}
|
224
|
+
RestClient::Resource.new(@api_endpoint.merge(path).to_s, options)
|
225
|
+
end
|
226
|
+
|
227
|
+
def rest_client
|
228
|
+
@rest_client ||= begin
|
229
|
+
create_rest_client("#{@api_endpoint.path}/#{@api_version}")
|
230
|
+
end
|
231
|
+
end
|
232
|
+
|
233
|
+
# Accepts the following string options:
|
234
|
+
# :namespace - the namespace of the entity.
|
235
|
+
# :name - the name of the entity to watch.
|
236
|
+
# :label_selector - a selector to restrict the list of returned objects by their labels.
|
237
|
+
# :field_selector - a selector to restrict the list of returned objects by their fields.
|
238
|
+
# :resource_version - shows changes that occur after that particular version of a resource.
|
239
|
+
def watch_entities(resource_name, options = {})
|
240
|
+
ns = build_namespace_prefix(options[:namespace])
|
241
|
+
|
242
|
+
path = "watch/#{ns}#{resource_name}"
|
243
|
+
path += "/#{options[:name]}" if options[:name]
|
244
|
+
uri = @api_endpoint.merge("#{@api_endpoint.path}/#{@api_version}/#{path}")
|
245
|
+
|
246
|
+
params = {}
|
247
|
+
WATCH_ARGUMENTS.each { |k, v| params[k] = options[v] if options[v] }
|
248
|
+
uri.query = URI.encode_www_form(params) if params.any?
|
249
|
+
|
250
|
+
Kubeclient::Common::WatchStream.new(uri, http_options(uri))
|
251
|
+
end
|
252
|
+
|
253
|
+
# Accepts the following string options:
|
254
|
+
# :namespace - the namespace of the entity.
|
255
|
+
# :label_selector - a selector to restrict the list of returned objects by their labels.
|
256
|
+
# :field_selector - a selector to restrict the list of returned objects by their fields.
|
257
|
+
def get_entities(entity_type, klass, resource_name, options = {})
|
258
|
+
params = {}
|
259
|
+
SEARCH_ARGUMENTS.each { |k, v| params[k] = options[v] if options[v] }
|
260
|
+
|
261
|
+
ns_prefix = build_namespace_prefix(options[:namespace])
|
262
|
+
|
263
|
+
response = handle_exception do
|
264
|
+
rest_client[ns_prefix + resource_name]
|
265
|
+
.get({ 'params' => params }.merge(@headers))
|
266
|
+
end
|
267
|
+
|
268
|
+
result = JSON.parse(response)
|
269
|
+
|
270
|
+
resource_version =
|
271
|
+
result.fetch('resourceVersion') do
|
272
|
+
result.fetch('metadata', {}).fetch('resourceVersion', nil)
|
273
|
+
end
|
274
|
+
|
275
|
+
# result['items'] might be nil due to https://github.com/kubernetes/kubernetes/issues/13096
|
276
|
+
collection = result['items'].to_a.map { |item| new_entity(item, klass) }
|
277
|
+
|
278
|
+
Kubeclient::Common::EntityList.new(entity_type, resource_version, collection)
|
279
|
+
end
|
280
|
+
|
281
|
+
def get_entity(klass, resource_name, name, namespace = nil)
|
282
|
+
ns_prefix = build_namespace_prefix(namespace)
|
283
|
+
response = handle_exception do
|
284
|
+
rest_client[ns_prefix + resource_name + "/#{name}"]
|
285
|
+
.get(@headers)
|
286
|
+
end
|
287
|
+
result = JSON.parse(response)
|
288
|
+
new_entity(result, klass)
|
289
|
+
end
|
290
|
+
|
291
|
+
def delete_entity(resource_name, name, namespace = nil)
|
292
|
+
ns_prefix = build_namespace_prefix(namespace)
|
293
|
+
handle_exception do
|
294
|
+
rest_client[ns_prefix + resource_name + "/#{name}"]
|
295
|
+
.delete(@headers)
|
296
|
+
end
|
297
|
+
end
|
298
|
+
|
299
|
+
def create_entity(entity_type, resource_name, entity_config, klass)
|
300
|
+
# Duplicate the entity_config to a hash so that when we assign
|
301
|
+
# kind and apiVersion, this does not mutate original entity_config obj.
|
302
|
+
hash = entity_config.to_hash
|
303
|
+
|
304
|
+
ns_prefix = build_namespace_prefix(hash[:metadata][:namespace])
|
305
|
+
|
306
|
+
# TODO: temporary solution to add "kind" and apiVersion to request
|
307
|
+
# until this issue is solved
|
308
|
+
# https://github.com/GoogleCloudPlatform/kubernetes/issues/6439
|
309
|
+
# TODO: #2 solution for
|
310
|
+
# https://github.com/kubernetes/kubernetes/issues/8115
|
311
|
+
hash[:kind] = (entity_type.eql?('Endpoint') ? 'Endpoints' : entity_type)
|
312
|
+
hash[:apiVersion] = @api_group + @api_version
|
313
|
+
response = handle_exception do
|
314
|
+
rest_client[ns_prefix + resource_name]
|
315
|
+
.post(hash.to_json, { 'Content-Type' => 'application/json' }.merge(@headers))
|
316
|
+
end
|
317
|
+
result = JSON.parse(response)
|
318
|
+
new_entity(result, klass)
|
319
|
+
end
|
320
|
+
|
321
|
+
def update_entity(resource_name, entity_config)
|
322
|
+
name = entity_config[:metadata][:name]
|
323
|
+
ns_prefix = build_namespace_prefix(entity_config[:metadata][:namespace])
|
324
|
+
handle_exception do
|
325
|
+
rest_client[ns_prefix + resource_name + "/#{name}"]
|
326
|
+
.put(entity_config.to_h.to_json, { 'Content-Type' => 'application/json' }.merge(@headers))
|
327
|
+
end
|
328
|
+
end
|
329
|
+
|
330
|
+
def patch_entity(resource_name, name, patch, namespace = nil)
|
331
|
+
ns_prefix = build_namespace_prefix(namespace)
|
332
|
+
|
333
|
+
handle_exception do
|
334
|
+
rest_client[ns_prefix + resource_name + "/#{name}"]
|
335
|
+
.patch(
|
336
|
+
patch.to_json,
|
337
|
+
{ 'Content-Type' => 'application/strategic-merge-patch+json' }.merge(@headers)
|
338
|
+
)
|
339
|
+
end
|
340
|
+
end
|
341
|
+
|
342
|
+
def rollback_entity(resource_name, name, entity_config: {}, namespace: nil)
|
343
|
+
ns_prefix = build_namespace_prefix(namespace)
|
344
|
+
|
345
|
+
hash = entity_config.to_hash
|
346
|
+
kind = resource_name.eql?('deployments') ? 'deployment' : resource_name
|
347
|
+
hash[:kind] = "#{kind.capitalize}Rollback"
|
348
|
+
hash[:apiVersion] = "extensions/v1beta1"
|
349
|
+
hash[:rollbackTo] ||= {}
|
350
|
+
hash[:name] ||= name
|
351
|
+
|
352
|
+
handle_exception do
|
353
|
+
rest_client[ns_prefix + resource_name + "/#{name}/rollback"]
|
354
|
+
.post(
|
355
|
+
hash.to_json,
|
356
|
+
{ 'Content-Type' => 'application/json' }.merge(@headers)
|
357
|
+
)
|
358
|
+
end
|
359
|
+
end
|
360
|
+
|
361
|
+
def new_entity(hash, klass)
|
362
|
+
klass.new(hash)
|
363
|
+
end
|
364
|
+
|
365
|
+
def all_entities
|
366
|
+
discover unless @discovered
|
367
|
+
@entities.values.each_with_object({}) do |entity, result_hash|
|
368
|
+
# method call for get each entities
|
369
|
+
# build hash of entity name to array of the entities
|
370
|
+
method_name = "get_#{entity.method_names[1]}"
|
371
|
+
begin
|
372
|
+
result_hash[entity.method_names[0]] = send(method_name)
|
373
|
+
rescue KubeException
|
374
|
+
next # do not fail due to resources not supporting get
|
375
|
+
end
|
376
|
+
end
|
377
|
+
end
|
378
|
+
|
379
|
+
def get_pod_log(pod_name, namespace, container: nil, previous: false)
|
380
|
+
params = {}
|
381
|
+
params[:previous] = true if previous
|
382
|
+
params[:container] = container if container
|
383
|
+
|
384
|
+
ns = build_namespace_prefix(namespace)
|
385
|
+
handle_exception do
|
386
|
+
rest_client[ns + "pods/#{pod_name}/log"]
|
387
|
+
.get({ 'params' => params }.merge(@headers))
|
388
|
+
end
|
389
|
+
end
|
390
|
+
|
391
|
+
def watch_pod_log(pod_name, namespace, container: nil)
|
392
|
+
# Adding the "follow=true" query param tells the Kubernetes API to keep
|
393
|
+
# the connection open and stream updates to the log.
|
394
|
+
params = { follow: true }
|
395
|
+
params[:container] = container if container
|
396
|
+
|
397
|
+
ns = build_namespace_prefix(namespace)
|
398
|
+
|
399
|
+
uri = @api_endpoint.dup
|
400
|
+
uri.path += "/#{@api_version}/#{ns}pods/#{pod_name}/log"
|
401
|
+
uri.query = URI.encode_www_form(params)
|
402
|
+
|
403
|
+
Kubeclient::Common::WatchStream.new(uri, http_options(uri), format: :text)
|
404
|
+
end
|
405
|
+
|
406
|
+
def proxy_url(kind, name, port, namespace = '')
|
407
|
+
discover unless @discovered
|
408
|
+
entity_name_plural =
|
409
|
+
if %w(services pods nodes).include?(kind.to_s)
|
410
|
+
kind.to_s
|
411
|
+
else
|
412
|
+
@entities[kind.to_s].resource_name
|
413
|
+
end
|
414
|
+
ns_prefix = build_namespace_prefix(namespace)
|
415
|
+
# TODO: Change this once services supports the new scheme
|
416
|
+
if entity_name_plural == 'pods'
|
417
|
+
rest_client["#{ns_prefix}#{entity_name_plural}/#{name}:#{port}/proxy"].url
|
418
|
+
else
|
419
|
+
rest_client["proxy/#{ns_prefix}#{entity_name_plural}/#{name}:#{port}"].url
|
420
|
+
end
|
421
|
+
end
|
422
|
+
|
423
|
+
def process_template(template)
|
424
|
+
ns_prefix = build_namespace_prefix(template[:metadata][:namespace])
|
425
|
+
response = handle_exception do
|
426
|
+
rest_client[ns_prefix + 'processedtemplates']
|
427
|
+
.post(template.to_h.to_json, { 'Content-Type' => 'application/json' }.merge(@headers))
|
428
|
+
end
|
429
|
+
JSON.parse(response)
|
430
|
+
end
|
431
|
+
|
432
|
+
def api_valid?
|
433
|
+
result = api
|
434
|
+
result.is_a?(Hash) && (result['versions'] || []).any? do |group|
|
435
|
+
@api_group.empty? ? group.include?(@api_version) : group['version'] == @api_version
|
436
|
+
end
|
437
|
+
end
|
438
|
+
|
439
|
+
def api
|
440
|
+
response = handle_exception { create_rest_client.get(@headers) }
|
441
|
+
JSON.parse(response)
|
442
|
+
end
|
443
|
+
|
444
|
+
private
|
445
|
+
|
446
|
+
def load_entities
|
447
|
+
@entities = {}
|
448
|
+
fetch_entities['resources'].each do |resource|
|
449
|
+
next if resource['name'].include?('/')
|
450
|
+
resource['kind'] ||=
|
451
|
+
Kubeclient::Common::MissingKindCompatibility.resource_kind(resource['name'])
|
452
|
+
entity = ClientMixin.parse_definition(resource['kind'], resource['name'])
|
453
|
+
@entities[entity.method_names[0]] = entity if entity
|
454
|
+
end
|
455
|
+
end
|
456
|
+
|
457
|
+
def fetch_entities
|
458
|
+
JSON.parse(handle_exception { rest_client.get(@headers) })
|
459
|
+
end
|
460
|
+
|
461
|
+
def bearer_token(bearer_token)
|
462
|
+
@headers ||= {}
|
463
|
+
@headers[:Authorization] = "Bearer #{bearer_token}"
|
464
|
+
end
|
465
|
+
|
466
|
+
def validate_auth_options(opts)
|
467
|
+
# maintain backward compatibility:
|
468
|
+
opts[:username] = opts[:user] if opts[:user]
|
469
|
+
|
470
|
+
if [:bearer_token, :bearer_token_file, :username].count { |key| opts[key] } > 1
|
471
|
+
raise(
|
472
|
+
ArgumentError,
|
473
|
+
'Invalid auth options: specify only one of username/password,' \
|
474
|
+
' bearer_token or bearer_token_file'
|
475
|
+
)
|
476
|
+
elsif [:username, :password].count { |key| opts[key] } == 1
|
477
|
+
raise ArgumentError, 'Basic auth requires both username & password'
|
478
|
+
end
|
479
|
+
end
|
480
|
+
|
481
|
+
def validate_bearer_token_file
|
482
|
+
msg = "Token file #{@auth_options[:bearer_token_file]} does not exist"
|
483
|
+
raise ArgumentError, msg unless File.file?(@auth_options[:bearer_token_file])
|
484
|
+
|
485
|
+
msg = "Cannot read token file #{@auth_options[:bearer_token_file]}"
|
486
|
+
raise ArgumentError, msg unless File.readable?(@auth_options[:bearer_token_file])
|
487
|
+
end
|
488
|
+
|
489
|
+
def http_options(uri)
|
490
|
+
options = {
|
491
|
+
basic_auth_user: @auth_options[:username],
|
492
|
+
basic_auth_password: @auth_options[:password],
|
493
|
+
headers: @headers,
|
494
|
+
http_proxy_uri: @http_proxy_uri
|
495
|
+
}
|
496
|
+
|
497
|
+
if uri.scheme == 'https'
|
498
|
+
options[:ssl] = {
|
499
|
+
ca_file: @ssl_options[:ca_file],
|
500
|
+
cert: @ssl_options[:client_cert],
|
501
|
+
cert_store: @ssl_options[:cert_store],
|
502
|
+
key: @ssl_options[:client_key],
|
503
|
+
# ruby HTTP uses verify_mode instead of verify_ssl
|
504
|
+
# http://ruby-doc.org/stdlib-1.9.3/libdoc/openssl/rdoc/OpenSSL/SSL/SSLContext.html
|
505
|
+
verify_mode: @ssl_options[:verify_ssl]
|
506
|
+
}
|
507
|
+
end
|
508
|
+
|
509
|
+
options.merge(@socket_options)
|
510
|
+
end
|
511
|
+
end
|
512
|
+
end
|
@@ -0,0 +1,126 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
require 'base64'
|
3
|
+
require 'pathname'
|
4
|
+
|
5
|
+
module Kubeclient
|
6
|
+
# Kubernetes client configuration class
|
7
|
+
class Config
|
8
|
+
# Kubernetes client configuration context class
|
9
|
+
class Context
|
10
|
+
attr_reader :api_endpoint, :api_version, :ssl_options, :auth_options
|
11
|
+
|
12
|
+
def initialize(api_endpoint, api_version, ssl_options, auth_options)
|
13
|
+
@api_endpoint = api_endpoint
|
14
|
+
@api_version = api_version
|
15
|
+
@ssl_options = ssl_options
|
16
|
+
@auth_options = auth_options
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def initialize(kcfg, kcfg_path)
|
21
|
+
@kcfg = kcfg
|
22
|
+
@kcfg_path = kcfg_path
|
23
|
+
raise 'Unknown kubeconfig version' if @kcfg['apiVersion'] != 'v1'
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.read(filename)
|
27
|
+
Config.new(YAML.load_file(filename), File.dirname(filename))
|
28
|
+
end
|
29
|
+
|
30
|
+
def contexts
|
31
|
+
@kcfg['contexts'].map { |x| x['name'] }
|
32
|
+
end
|
33
|
+
|
34
|
+
def context(context_name = nil)
|
35
|
+
cluster, user = fetch_context(context_name || @kcfg['current-context'])
|
36
|
+
|
37
|
+
ca_cert_data = fetch_cluster_ca_data(cluster)
|
38
|
+
client_cert_data = fetch_user_cert_data(user)
|
39
|
+
client_key_data = fetch_user_key_data(user)
|
40
|
+
auth_options = fetch_user_auth_options(user)
|
41
|
+
|
42
|
+
ssl_options = {}
|
43
|
+
|
44
|
+
if !ca_cert_data.nil?
|
45
|
+
cert_store = OpenSSL::X509::Store.new
|
46
|
+
cert_store.add_cert(OpenSSL::X509::Certificate.new(ca_cert_data))
|
47
|
+
ssl_options[:verify_ssl] = OpenSSL::SSL::VERIFY_PEER
|
48
|
+
ssl_options[:cert_store] = cert_store
|
49
|
+
else
|
50
|
+
ssl_options[:verify_ssl] = OpenSSL::SSL::VERIFY_NONE
|
51
|
+
end
|
52
|
+
|
53
|
+
unless client_cert_data.nil?
|
54
|
+
ssl_options[:client_cert] = OpenSSL::X509::Certificate.new(client_cert_data)
|
55
|
+
end
|
56
|
+
|
57
|
+
unless client_key_data.nil?
|
58
|
+
ssl_options[:client_key] = OpenSSL::PKey.read(client_key_data)
|
59
|
+
end
|
60
|
+
|
61
|
+
Context.new(cluster['server'], @kcfg['apiVersion'], ssl_options, auth_options)
|
62
|
+
end
|
63
|
+
|
64
|
+
private
|
65
|
+
|
66
|
+
def ext_file_path(path)
|
67
|
+
Pathname(path).absolute? ? path : File.join(@kcfg_path, path)
|
68
|
+
end
|
69
|
+
|
70
|
+
def fetch_context(context_name)
|
71
|
+
context = @kcfg['contexts'].detect do |x|
|
72
|
+
break x['context'] if x['name'] == context_name
|
73
|
+
end
|
74
|
+
|
75
|
+
raise "Unknown context #{context_name}" unless context
|
76
|
+
|
77
|
+
cluster = @kcfg['clusters'].detect do |x|
|
78
|
+
break x['cluster'] if x['name'] == context['cluster']
|
79
|
+
end
|
80
|
+
|
81
|
+
raise "Unknown cluster #{context['cluster']}" unless cluster
|
82
|
+
|
83
|
+
user = @kcfg['users'].detect do |x|
|
84
|
+
break x['user'] if x['name'] == context['user']
|
85
|
+
end || {}
|
86
|
+
|
87
|
+
[cluster, user]
|
88
|
+
end
|
89
|
+
|
90
|
+
def fetch_cluster_ca_data(cluster)
|
91
|
+
if cluster.key?('certificate-authority')
|
92
|
+
File.read(ext_file_path(cluster['certificate-authority']))
|
93
|
+
elsif cluster.key?('certificate-authority-data')
|
94
|
+
Base64.decode64(cluster['certificate-authority-data'])
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
def fetch_user_cert_data(user)
|
99
|
+
if user.key?('client-certificate')
|
100
|
+
File.read(ext_file_path(user['client-certificate']))
|
101
|
+
elsif user.key?('client-certificate-data')
|
102
|
+
Base64.decode64(user['client-certificate-data'])
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
def fetch_user_key_data(user)
|
107
|
+
if user.key?('client-key')
|
108
|
+
File.read(ext_file_path(user['client-key']))
|
109
|
+
elsif user.key?('client-key-data')
|
110
|
+
Base64.decode64(user['client-key-data'])
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
def fetch_user_auth_options(user)
|
115
|
+
options = {}
|
116
|
+
if user.key?('token')
|
117
|
+
options[:bearer_token] = user['token']
|
118
|
+
else
|
119
|
+
%w(username password).each do |attr|
|
120
|
+
options[attr.to_sym] = user[attr] if user.key?(attr)
|
121
|
+
end
|
122
|
+
end
|
123
|
+
options
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
require 'delegate'
|
2
|
+
module Kubeclient
|
3
|
+
module Common
|
4
|
+
# Kubernetes Entity List
|
5
|
+
class EntityList < DelegateClass(Array)
|
6
|
+
attr_reader :kind, :resourceVersion
|
7
|
+
|
8
|
+
def initialize(kind, resource_version, list)
|
9
|
+
@kind = kind
|
10
|
+
# rubocop:disable Style/VariableName
|
11
|
+
@resourceVersion = resource_version
|
12
|
+
super(list)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
# Kubernetes HTTP Exceptions
|
2
|
+
class KubeException < StandardError
|
3
|
+
attr_reader :error_code, :message, :response
|
4
|
+
|
5
|
+
def initialize(error_code, message, response)
|
6
|
+
@error_code = error_code
|
7
|
+
@message = message
|
8
|
+
@response = response
|
9
|
+
end
|
10
|
+
|
11
|
+
def to_s
|
12
|
+
'HTTP status code ' + @error_code.to_s + ', ' + @message
|
13
|
+
end
|
14
|
+
end
|