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.
@@ -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