k8s-client-renewed 0.10.5.pre.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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