k8s-client-renewed 0.10.5.pre.1
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/.rspec +3 -0
- data/.rubocop.relaxed.yml +176 -0
- data/.rubocop.yml +57 -0
- data/.travis.yml +20 -0
- data/Dockerfile +11 -0
- data/Gemfile +6 -0
- data/LICENSE +201 -0
- data/README.md +233 -0
- data/Rakefile +16 -0
- data/docker-compose.yaml +10 -0
- data/k8s-client.gemspec +39 -0
- data/lib/k8s-client.rb +3 -0
- data/lib/k8s/api_client.rb +115 -0
- data/lib/k8s/client.rb +282 -0
- data/lib/k8s/client/version.rb +8 -0
- data/lib/k8s/config.rb +168 -0
- data/lib/k8s/error.rb +66 -0
- data/lib/k8s/json_parser.rb +97 -0
- data/lib/k8s/json_parser/yajl.rb +44 -0
- data/lib/k8s/logging.rb +87 -0
- data/lib/k8s/resource.rb +111 -0
- data/lib/k8s/resource_client.rb +349 -0
- data/lib/k8s/stack.rb +187 -0
- data/lib/k8s/transport.rb +380 -0
- data/lib/k8s/util.rb +178 -0
- data/lib/k8s/watch_event.rb +12 -0
- metadata +224 -0
data/lib/k8s/resource.rb
ADDED
@@ -0,0 +1,111 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'recursive-open-struct'
|
4
|
+
require 'hashdiff'
|
5
|
+
require 'yaml/safe_load_stream'
|
6
|
+
|
7
|
+
module K8s
|
8
|
+
# generic untyped resource
|
9
|
+
class Resource < RecursiveOpenStruct
|
10
|
+
using YAMLSafeLoadStream
|
11
|
+
using K8s::Util::HashBackport if RUBY_VERSION < "2.5"
|
12
|
+
|
13
|
+
include Comparable
|
14
|
+
|
15
|
+
# @param data [String]
|
16
|
+
# @return [self]
|
17
|
+
def self.from_json(data)
|
18
|
+
new(K8s::JSONParser.parse(data))
|
19
|
+
end
|
20
|
+
|
21
|
+
# @param filename [String] file path
|
22
|
+
# @return [K8s::Resource]
|
23
|
+
def self.from_file(filename)
|
24
|
+
new(YAML.safe_load(File.read(filename), [], [], true, filename))
|
25
|
+
end
|
26
|
+
|
27
|
+
# @param path [String] file path
|
28
|
+
# @return [Array<K8s::Resource>]
|
29
|
+
def self.from_files(path)
|
30
|
+
stat = File.stat(path)
|
31
|
+
|
32
|
+
if stat.directory?
|
33
|
+
# recurse
|
34
|
+
Dir.glob("#{path}/*.{yml,yaml}").sort.map { |dir| from_files(dir) }.flatten
|
35
|
+
else
|
36
|
+
YAML.safe_load_stream(File.read(path), path).map{ |doc| new(doc) }
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def self.default_options
|
41
|
+
{
|
42
|
+
mutate_input_hash: false,
|
43
|
+
recurse_over_arrays: true,
|
44
|
+
preserve_original_keys: false
|
45
|
+
}
|
46
|
+
end
|
47
|
+
|
48
|
+
# @param hash [Hash]
|
49
|
+
# @param recurse_over_arrays [Boolean]
|
50
|
+
# @param options [Hash] see RecursiveOpenStruct#initialize
|
51
|
+
def initialize hash, options = {}
|
52
|
+
super(
|
53
|
+
hash.is_a?(Hash) ? hash : hash.to_h,
|
54
|
+
options
|
55
|
+
)
|
56
|
+
end
|
57
|
+
|
58
|
+
def <=>(other)
|
59
|
+
to_h <=> (other.is_a?(Hash) ? other : other.to_h)
|
60
|
+
end
|
61
|
+
|
62
|
+
# @param options [Hash] see Hash#to_json
|
63
|
+
# @return [String]
|
64
|
+
def to_json(options = {})
|
65
|
+
to_h.to_json(options)
|
66
|
+
end
|
67
|
+
|
68
|
+
# merge in fields
|
69
|
+
#
|
70
|
+
# @param attrs [Hash, K8s::Resource]
|
71
|
+
# @return [K8s::Resource]
|
72
|
+
def merge(attrs)
|
73
|
+
self.class.new(
|
74
|
+
Util.deep_merge(to_hash, attrs.to_hash, overwrite_arrays: true, merge_nil_values: true)
|
75
|
+
)
|
76
|
+
end
|
77
|
+
|
78
|
+
# @return [String]
|
79
|
+
def checksum
|
80
|
+
@checksum ||= Digest::MD5.hexdigest(Marshal.dump(to_h))
|
81
|
+
end
|
82
|
+
|
83
|
+
# @param attrs [Hash]
|
84
|
+
# @param config_annotation [String]
|
85
|
+
# @return [Hash]
|
86
|
+
def merge_patch_ops(attrs, config_annotation)
|
87
|
+
Util.json_patch(current_config(config_annotation), Util.deep_transform_keys(attrs, :to_s))
|
88
|
+
end
|
89
|
+
|
90
|
+
# Gets the existing resources (on kube api) configuration, an empty hash if not present
|
91
|
+
#
|
92
|
+
# @param config_annotation [String]
|
93
|
+
# @return [Hash]
|
94
|
+
def current_config(config_annotation)
|
95
|
+
current_cfg = metadata.annotations&.dig(config_annotation)
|
96
|
+
return {} unless current_cfg
|
97
|
+
|
98
|
+
current_hash = K8s::JSONParser.parse(current_cfg)
|
99
|
+
# kubectl adds empty metadata.namespace, let's fix it
|
100
|
+
current_hash['metadata'].delete('namespace') if current_hash.dig('metadata', 'namespace').to_s.empty?
|
101
|
+
|
102
|
+
current_hash
|
103
|
+
end
|
104
|
+
|
105
|
+
# @param config_annotation [String]
|
106
|
+
# @return [Boolean]
|
107
|
+
def can_patch?(config_annotation)
|
108
|
+
!!metadata.annotations&.dig(config_annotation)
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
@@ -0,0 +1,349 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module K8s
|
4
|
+
# Per-APIResource type client.
|
5
|
+
#
|
6
|
+
# Used to get/list/update/patch/delete specific types of resources, optionally in some specific namespace.
|
7
|
+
class ResourceClient
|
8
|
+
# Common helpers used in both class/instance methods
|
9
|
+
module Utils
|
10
|
+
# @param selector [NilClass, String, Hash{String => String}]
|
11
|
+
# @return [NilClass, String]
|
12
|
+
def selector_query(selector)
|
13
|
+
case selector
|
14
|
+
when nil
|
15
|
+
nil
|
16
|
+
when Symbol
|
17
|
+
selector.to_s
|
18
|
+
when String
|
19
|
+
selector
|
20
|
+
when Hash
|
21
|
+
selector.map{ |k, v| "#{k}=#{v}" }.join ','
|
22
|
+
else
|
23
|
+
fail "Invalid selector type. #{selector.inspect}"
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
# @param options [Hash]
|
28
|
+
# @return [Hash, NilClass]
|
29
|
+
def make_query(options)
|
30
|
+
query = options.compact
|
31
|
+
|
32
|
+
return nil if query.empty?
|
33
|
+
|
34
|
+
query
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
include Utils
|
39
|
+
extend Utils
|
40
|
+
|
41
|
+
# Pipeline list requests for multiple resource types.
|
42
|
+
#
|
43
|
+
# Returns flattened array with mixed resource kinds.
|
44
|
+
#
|
45
|
+
# @param resources [Array<K8s::ResourceClient>]
|
46
|
+
# @param transport [K8s::Transport]
|
47
|
+
# @param namespace [String, nil]
|
48
|
+
# @param labelSelector [nil, String, Hash{String => String}]
|
49
|
+
# @param fieldSelector [nil, String, Hash{String => String}]
|
50
|
+
# @param skip_forbidden [Boolean] skip resources that return HTTP 403 errors
|
51
|
+
# @return [Array<K8s::Resource>]
|
52
|
+
def self.list(resources, transport, namespace: nil, labelSelector: nil, fieldSelector: nil, skip_forbidden: false)
|
53
|
+
api_paths = resources.map{ |resource| resource.path(namespace: namespace) }
|
54
|
+
api_lists = transport.gets(
|
55
|
+
*api_paths,
|
56
|
+
response_class: K8s::Resource,
|
57
|
+
query: make_query(
|
58
|
+
'labelSelector' => selector_query(labelSelector),
|
59
|
+
'fieldSelector' => selector_query(fieldSelector)
|
60
|
+
),
|
61
|
+
skip_forbidden: skip_forbidden
|
62
|
+
)
|
63
|
+
|
64
|
+
resources.zip(api_lists).map { |resource, api_list| api_list ? resource.process_list(api_list) : [] }.flatten
|
65
|
+
end
|
66
|
+
|
67
|
+
# @param transport [K8s::Transport]
|
68
|
+
# @param api_client [K8s::APIClient]
|
69
|
+
# @param api_resource [K8s::Resource]
|
70
|
+
# @param namespace [String]
|
71
|
+
# @param resource_class [Class]
|
72
|
+
def initialize(transport, api_client, api_resource, namespace: nil, resource_class: K8s::Resource)
|
73
|
+
@transport = transport
|
74
|
+
@api_client = api_client
|
75
|
+
@api_resource = api_resource
|
76
|
+
@namespace = namespace
|
77
|
+
@resource_class = resource_class
|
78
|
+
|
79
|
+
if @api_resource.name.include? '/'
|
80
|
+
@resource, @subresource = @api_resource.name.split('/', 2)
|
81
|
+
else
|
82
|
+
@resource = @api_resource.name
|
83
|
+
@subresource = nil
|
84
|
+
end
|
85
|
+
|
86
|
+
fail "Resource #{api_resource.name} is not namespaced" unless api_resource.namespaced || !namespace
|
87
|
+
end
|
88
|
+
|
89
|
+
# @return [String]
|
90
|
+
def api_version
|
91
|
+
@api_client.api_version
|
92
|
+
end
|
93
|
+
|
94
|
+
# @return [String] resource or resource/subresource
|
95
|
+
def name
|
96
|
+
@api_resource.name
|
97
|
+
end
|
98
|
+
|
99
|
+
# @return [String, nil]
|
100
|
+
attr_reader :namespace
|
101
|
+
|
102
|
+
# @return [String]
|
103
|
+
attr_reader :resource
|
104
|
+
|
105
|
+
# @return [Boolean]
|
106
|
+
def subresource?
|
107
|
+
!!@subresource
|
108
|
+
end
|
109
|
+
|
110
|
+
# @return [String, nil]
|
111
|
+
attr_reader :subresource
|
112
|
+
|
113
|
+
# @return [String]
|
114
|
+
def kind
|
115
|
+
@api_resource.kind
|
116
|
+
end
|
117
|
+
|
118
|
+
# @return [class] K8s::Resource
|
119
|
+
attr_reader :resource_class
|
120
|
+
|
121
|
+
# @return [Boolean]
|
122
|
+
def namespaced?
|
123
|
+
!!@api_resource.namespaced
|
124
|
+
end
|
125
|
+
|
126
|
+
# @param name [NilClass, String]
|
127
|
+
# @param subresource [String, NilClass]
|
128
|
+
# @param namespace [String, NilClass]
|
129
|
+
# @return [String]
|
130
|
+
def path(name = nil, subresource: @subresource, namespace: @namespace)
|
131
|
+
namespace_part = namespace ? ['namespaces', namespace] : []
|
132
|
+
|
133
|
+
if name && subresource
|
134
|
+
@api_client.path(*namespace_part, @resource, name, subresource)
|
135
|
+
elsif name
|
136
|
+
@api_client.path(*namespace_part, @resource, name)
|
137
|
+
else
|
138
|
+
@api_client.path(*namespace_part, @resource)
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
# @return [Bool]
|
143
|
+
def create?
|
144
|
+
@api_resource.verbs.include? 'create'
|
145
|
+
end
|
146
|
+
|
147
|
+
# @param resource [#metadata] with metadata.namespace and metadata.name set
|
148
|
+
# @return [Object] instance of resource_class
|
149
|
+
def create_resource(resource)
|
150
|
+
@transport.request(
|
151
|
+
method: 'POST',
|
152
|
+
path: path(namespace: resource.metadata.namespace),
|
153
|
+
request_object: resource,
|
154
|
+
response_class: @resource_class
|
155
|
+
)
|
156
|
+
end
|
157
|
+
|
158
|
+
# @return [Bool]
|
159
|
+
def get?
|
160
|
+
@api_resource.verbs.include? 'get'
|
161
|
+
end
|
162
|
+
|
163
|
+
# @param name [String]
|
164
|
+
# @param namespace [String, NilClass]
|
165
|
+
# @return [Object] instance of resource_class
|
166
|
+
def get(name, namespace: @namespace)
|
167
|
+
@transport.request(
|
168
|
+
method: 'GET',
|
169
|
+
path: path(name, namespace: namespace),
|
170
|
+
response_class: @resource_class
|
171
|
+
)
|
172
|
+
end
|
173
|
+
|
174
|
+
# @param resource [resource_class]
|
175
|
+
# @return [Object] instance of resource_class
|
176
|
+
def get_resource(resource)
|
177
|
+
@transport.request(
|
178
|
+
method: 'GET',
|
179
|
+
path: path(resource.metadata.name, namespace: resource.metadata.namespace),
|
180
|
+
response_class: @resource_class
|
181
|
+
)
|
182
|
+
end
|
183
|
+
|
184
|
+
# @return [Bool]
|
185
|
+
def list?
|
186
|
+
@api_resource.verbs.include? 'list'
|
187
|
+
end
|
188
|
+
|
189
|
+
# @param list [K8s::Resource]
|
190
|
+
# @return [Array<Object>] array of instances of resource_class
|
191
|
+
def process_list(list)
|
192
|
+
list.items.map { |item|
|
193
|
+
# list items omit kind/apiVersion
|
194
|
+
@resource_class.new(item.merge('apiVersion' => list.apiVersion, 'kind' => @api_resource.kind))
|
195
|
+
}
|
196
|
+
end
|
197
|
+
|
198
|
+
# @param labelSelector [nil, String, Hash{String => String}]
|
199
|
+
# @param fieldSelector [nil, String, Hash{String => String}]
|
200
|
+
# @param namespace [nil, String]
|
201
|
+
# @return [Array<Object>] array of instances of resource_class
|
202
|
+
def list(labelSelector: nil, fieldSelector: nil, namespace: @namespace)
|
203
|
+
list = meta_list(labelSelector: labelSelector, fieldSelector: fieldSelector, namespace: namespace)
|
204
|
+
process_list(list)
|
205
|
+
end
|
206
|
+
|
207
|
+
# @param labelSelector [nil, String, Hash{String => String}]
|
208
|
+
# @param fieldSelector [nil, String, Hash{String => String}]
|
209
|
+
# @param namespace [nil, String]
|
210
|
+
# @return [K8s::Resource]
|
211
|
+
def meta_list(labelSelector: nil, fieldSelector: nil, namespace: @namespace)
|
212
|
+
@transport.request(
|
213
|
+
method: 'GET',
|
214
|
+
path: path(namespace: namespace),
|
215
|
+
query: make_query(
|
216
|
+
'labelSelector' => selector_query(labelSelector),
|
217
|
+
'fieldSelector' => selector_query(fieldSelector)
|
218
|
+
)
|
219
|
+
)
|
220
|
+
end
|
221
|
+
|
222
|
+
# @param labelSelector [nil, String, Hash{String => String}]
|
223
|
+
# @param fieldSelector [nil, String, Hash{String => String}]
|
224
|
+
# @param resourceVersion [nil, String]
|
225
|
+
# @param timeout [nil, Integer]
|
226
|
+
# @yield [K8S::WatchEvent]
|
227
|
+
# @raise [Excon::Error]
|
228
|
+
def watch(labelSelector: nil, fieldSelector: nil, resourceVersion: nil, timeout: nil, namespace: @namespace)
|
229
|
+
method = 'GET'
|
230
|
+
path = path(namespace: namespace)
|
231
|
+
|
232
|
+
parser = K8s::JSONParser.new do |data|
|
233
|
+
yield K8s::WatchEvent.new(data)
|
234
|
+
end
|
235
|
+
|
236
|
+
@transport.request(
|
237
|
+
method: method,
|
238
|
+
path: path,
|
239
|
+
read_timeout: nil,
|
240
|
+
query: make_query(
|
241
|
+
'labelSelector' => selector_query(labelSelector),
|
242
|
+
'fieldSelector' => selector_query(fieldSelector),
|
243
|
+
'resourceVersion' => resourceVersion,
|
244
|
+
'watch' => '1',
|
245
|
+
'timeoutSeconds' => timeout
|
246
|
+
),
|
247
|
+
response_block: lambda do |chunk, _, _|
|
248
|
+
parser << chunk
|
249
|
+
end
|
250
|
+
)
|
251
|
+
end
|
252
|
+
|
253
|
+
# @return [Boolean]
|
254
|
+
def update?
|
255
|
+
@api_resource.verbs.include? 'update'
|
256
|
+
end
|
257
|
+
|
258
|
+
# @param resource [#metadata] with metadata.resourceVersion set
|
259
|
+
# @return [Object] instance of resource_class
|
260
|
+
def update_resource(resource)
|
261
|
+
@transport.request(
|
262
|
+
method: 'PUT',
|
263
|
+
path: path(resource.metadata.name, namespace: resource.metadata.namespace),
|
264
|
+
request_object: resource,
|
265
|
+
response_class: @resource_class
|
266
|
+
)
|
267
|
+
end
|
268
|
+
|
269
|
+
# @return [Boolean]
|
270
|
+
def patch?
|
271
|
+
@api_resource.verbs.include? 'patch'
|
272
|
+
end
|
273
|
+
|
274
|
+
# @param name [String]
|
275
|
+
# @param obj [#to_json]
|
276
|
+
# @param namespace [String, nil]
|
277
|
+
# @param strategic_merge [Boolean] use kube Strategic Merge Patch instead of standard Merge Patch (arrays of objects are merged by name)
|
278
|
+
# @return [Object] instance of resource_class
|
279
|
+
def merge_patch(name, obj, namespace: @namespace, strategic_merge: true)
|
280
|
+
@transport.request(
|
281
|
+
method: 'PATCH',
|
282
|
+
path: path(name, namespace: namespace),
|
283
|
+
content_type: strategic_merge ? 'application/strategic-merge-patch+json' : 'application/merge-patch+json',
|
284
|
+
request_object: obj,
|
285
|
+
response_class: @resource_class
|
286
|
+
)
|
287
|
+
end
|
288
|
+
|
289
|
+
# @param name [String]
|
290
|
+
# @param ops [Hash] json-patch operations
|
291
|
+
# @param namespace [String, nil]
|
292
|
+
# @return [Object] instance of resource_class
|
293
|
+
def json_patch(name, ops, namespace: @namespace)
|
294
|
+
@transport.request(
|
295
|
+
method: 'PATCH',
|
296
|
+
path: path(name, namespace: namespace),
|
297
|
+
content_type: 'application/json-patch+json',
|
298
|
+
request_object: ops,
|
299
|
+
response_class: @resource_class
|
300
|
+
)
|
301
|
+
end
|
302
|
+
|
303
|
+
# @return [Boolean]
|
304
|
+
def delete?
|
305
|
+
@api_resource.verbs.include? 'delete'
|
306
|
+
end
|
307
|
+
|
308
|
+
# @param name [String]
|
309
|
+
# @param namespace [String, nil]
|
310
|
+
# @param propagationPolicy [String, nil] The propagationPolicy to use for the API call. Possible values include “Orphan”, “Foreground”, or “Background”
|
311
|
+
# @return [K8s::Resource]
|
312
|
+
def delete(name, namespace: @namespace, propagationPolicy: nil)
|
313
|
+
@transport.request(
|
314
|
+
method: 'DELETE',
|
315
|
+
path: path(name, namespace: namespace),
|
316
|
+
query: make_query(
|
317
|
+
'propagationPolicy' => propagationPolicy
|
318
|
+
),
|
319
|
+
response_class: @resource_class
|
320
|
+
)
|
321
|
+
end
|
322
|
+
|
323
|
+
# @param namespace [String, nil]
|
324
|
+
# @param labelSelector [nil, String, Hash{String => String}]
|
325
|
+
# @param fieldSelector [nil, String, Hash{String => String}]
|
326
|
+
# @param propagationPolicy [String, nil]
|
327
|
+
# @return [Array<Object>] array of instances of resource_class
|
328
|
+
def delete_collection(namespace: @namespace, labelSelector: nil, fieldSelector: nil, propagationPolicy: nil)
|
329
|
+
list = @transport.request(
|
330
|
+
method: 'DELETE',
|
331
|
+
path: path(namespace: namespace),
|
332
|
+
query: make_query(
|
333
|
+
'labelSelector' => selector_query(labelSelector),
|
334
|
+
'fieldSelector' => selector_query(fieldSelector),
|
335
|
+
'propagationPolicy' => propagationPolicy
|
336
|
+
)
|
337
|
+
)
|
338
|
+
process_list(list)
|
339
|
+
end
|
340
|
+
|
341
|
+
# @param resource [resource_class] with metadata
|
342
|
+
# @param options [Hash]
|
343
|
+
# @see #delete for possible options
|
344
|
+
# @return [K8s::Resource]
|
345
|
+
def delete_resource(resource, **options)
|
346
|
+
delete(resource.metadata.name, namespace: resource.metadata.namespace, **options)
|
347
|
+
end
|
348
|
+
end
|
349
|
+
end
|