k8s-ruby2 0.10.6

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,115 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'recursive-open-struct'
4
+ require 'hashdiff'
5
+ require 'forwardable'
6
+ require 'yaml/safe_load_stream'
7
+
8
+ module K8s
9
+ # generic untyped resource
10
+ class Resource < RecursiveOpenStruct
11
+ extend Forwardable
12
+ include Comparable
13
+
14
+ using YAMLSafeLoadStream
15
+ using K8s::Util::HashDeepMerge
16
+
17
+ # @param data [String]
18
+ # @return [self]
19
+ def self.from_json(data)
20
+ new(Yajl::Parser.parse(data))
21
+ end
22
+
23
+ # @param filename [String] file path
24
+ # @return [K8s::Resource]
25
+ def self.from_file(filename)
26
+ new(YAML.safe_load(File.read(filename), [], [], true, filename))
27
+ end
28
+
29
+ # @param path [String] file path
30
+ # @return [Array<K8s::Resource>]
31
+ def self.from_files(path)
32
+ stat = File.stat(path)
33
+
34
+ if stat.directory?
35
+ # recurse
36
+ Dir.glob("#{path}/*.{yml,yaml}").sort.map { |dir| from_files(dir) }.flatten
37
+ else
38
+ YAML.safe_load_stream(File.read(path), path).map{ |doc| new(doc) }
39
+ end
40
+ end
41
+
42
+ # @param hash [Hash]
43
+ # @param recurse_over_arrays [Boolean]
44
+ # @param options [Hash] see RecursiveOpenStruct#initialize
45
+ def initialize(hash, options = {}, **kwargs)
46
+ options_with_defaults = { recurse_over_arrays: true }.merge(options).merge(kwargs)
47
+ super(hash, options_with_defaults)
48
+ end
49
+
50
+ # @param options [Hash] see Hash#to_json
51
+ # @return [String]
52
+ def to_json(**options)
53
+ to_hash.to_json(**options)
54
+ end
55
+
56
+ # @param other [K8s::Resource]
57
+ # @return [Boolean]
58
+ def <=>(other)
59
+ to_hash <=> other.to_hash
60
+ end
61
+
62
+ # merge in fields
63
+ #
64
+ # @param attrs [Hash, K8s::Resource]
65
+ # @return [K8s::Resource]
66
+ def merge(attrs)
67
+ # deep clone of attrs
68
+ h = to_hash
69
+
70
+ # merge in-place
71
+ h.deep_merge!(attrs.to_hash, overwrite_arrays: true, merge_nil_values: true)
72
+
73
+ self.class.new(h)
74
+ end
75
+
76
+ # @return [String]
77
+ def checksum
78
+ @checksum ||= Digest::MD5.hexdigest(Marshal.dump(to_hash))
79
+ end
80
+
81
+ # @param attrs [Hash]
82
+ # @param config_annotation [String]
83
+ # @return [Hash]
84
+ def merge_patch_ops(attrs, config_annotation)
85
+ Util.json_patch(current_config(config_annotation), stringify_hash(attrs))
86
+ end
87
+
88
+ # Gets the existing resources (on kube api) configuration, an empty hash if not present
89
+ #
90
+ # @param config_annotation [String]
91
+ # @return [Hash]
92
+ def current_config(config_annotation)
93
+ current_cfg = metadata.annotations&.dig(config_annotation)
94
+ return {} unless current_cfg
95
+
96
+ current_hash = Yajl::Parser.parse(current_cfg)
97
+ # kubectl adds empty metadata.namespace, let's fix it
98
+ current_hash['metadata'].delete('namespace') if current_hash.dig('metadata', 'namespace').to_s.empty?
99
+
100
+ current_hash
101
+ end
102
+
103
+ # @param config_annotation [String]
104
+ # @return [Boolean]
105
+ def can_patch?(config_annotation)
106
+ !!metadata.annotations&.dig(config_annotation)
107
+ end
108
+
109
+ # @param hash [Hash]
110
+ # @return [Hash]
111
+ def stringify_hash(hash)
112
+ Yajl::Parser.parse(JSON.dump(hash))
113
+ end
114
+ end
115
+ 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 String
17
+ selector
18
+ when Hash
19
+ selector.map{ |k, v| "#{k}=#{v}" }.join ','
20
+ else
21
+ fail "Invalid selector type. #{selector.inspect}"
22
+ end
23
+ end
24
+
25
+ # @param options [Hash]
26
+ # @return [Hash, NilClass]
27
+ def make_query(options)
28
+ query = options.compact
29
+
30
+ return nil if query.empty?
31
+
32
+ query
33
+ end
34
+ end
35
+
36
+ include Utils
37
+ extend Utils
38
+
39
+ # Pipeline list requests for multiple resource types.
40
+ #
41
+ # Returns flattened array with mixed resource kinds.
42
+ #
43
+ # @param resources [Array<K8s::ResourceClient>]
44
+ # @param transport [K8s::Transport]
45
+ # @param namespace [String, nil]
46
+ # @param labelSelector [nil, String, Hash{String => String}]
47
+ # @param fieldSelector [nil, String, Hash{String => String}]
48
+ # @param skip_forbidden [Boolean] skip resources that return HTTP 403 errors
49
+ # @return [Array<K8s::Resource>]
50
+ def self.list(resources, transport, namespace: nil, labelSelector: nil, fieldSelector: nil, skip_forbidden: false)
51
+ api_paths = resources.map{ |resource| resource.path(namespace: namespace) }
52
+ api_lists = transport.gets(
53
+ *api_paths,
54
+ response_class: K8s::API::MetaV1::List,
55
+ query: make_query(
56
+ 'labelSelector' => selector_query(labelSelector),
57
+ 'fieldSelector' => selector_query(fieldSelector)
58
+ ),
59
+ skip_forbidden: skip_forbidden
60
+ )
61
+
62
+ resources.zip(api_lists).map { |resource, api_list| api_list ? resource.process_list(api_list) : [] }.flatten
63
+ end
64
+
65
+ # @param transport [K8s::Transport]
66
+ # @param api_client [K8s::APIClient]
67
+ # @param api_resource [K8s::API::MetaV1::APIResource]
68
+ # @param namespace [String]
69
+ # @param resource_class [Class]
70
+ def initialize(transport, api_client, api_resource, namespace: nil, resource_class: K8s::Resource)
71
+ @transport = transport
72
+ @api_client = api_client
73
+ @api_resource = api_resource
74
+ @namespace = namespace
75
+ @resource_class = resource_class
76
+
77
+ if @api_resource.name.include? '/'
78
+ @resource, @subresource = @api_resource.name.split('/', 2)
79
+ else
80
+ @resource = @api_resource.name
81
+ @subresource = nil
82
+ end
83
+
84
+ fail "Resource #{api_resource.name} is not namespaced" unless api_resource.namespaced || !namespace
85
+ end
86
+
87
+ # @return [String]
88
+ def api_version
89
+ @api_client.api_version
90
+ end
91
+
92
+ # @return [String] resource or resource/subresource
93
+ def name
94
+ @api_resource.name
95
+ end
96
+
97
+ # @return [String, nil]
98
+ attr_reader :namespace
99
+
100
+ # @return [String]
101
+ attr_reader :resource
102
+
103
+ # @return [Boolean]
104
+ def subresource?
105
+ !!@subresource
106
+ end
107
+
108
+ # @return [String, nil]
109
+ attr_reader :subresource
110
+
111
+ # @return [String]
112
+ def kind
113
+ @api_resource.kind
114
+ end
115
+
116
+ # @return [class] K8s::Resource
117
+ attr_reader :resource_class
118
+
119
+ # @return [Boolean]
120
+ def namespaced?
121
+ !!@api_resource.namespaced
122
+ end
123
+
124
+ # @param name [NilClass, String]
125
+ # @param subresource [String, NilClass]
126
+ # @param namespace [String, NilClass]
127
+ # @return [String]
128
+ def path(name = nil, subresource: @subresource, namespace: @namespace)
129
+ namespace_part = namespace ? ['namespaces', namespace] : []
130
+
131
+ if name && subresource
132
+ @api_client.path(*namespace_part, @resource, name, subresource)
133
+ elsif name
134
+ @api_client.path(*namespace_part, @resource, name)
135
+ else
136
+ @api_client.path(*namespace_part, @resource)
137
+ end
138
+ end
139
+
140
+ # @return [Bool]
141
+ def create?
142
+ @api_resource.verbs.include? 'create'
143
+ end
144
+
145
+ # @param resource [#metadata] with metadata.namespace and metadata.name set
146
+ # @return [Object] instance of resource_class
147
+ def create_resource(resource)
148
+ @transport.request(
149
+ method: 'POST',
150
+ path: path(namespace: resource.metadata.namespace),
151
+ request_object: resource,
152
+ response_class: @resource_class
153
+ )
154
+ end
155
+
156
+ # @return [Bool]
157
+ def get?
158
+ @api_resource.verbs.include? 'get'
159
+ end
160
+
161
+ # @param name [String]
162
+ # @param namespace [String, NilClass]
163
+ # @return [Object] instance of resource_class
164
+ def get(name, namespace: @namespace)
165
+ @transport.request(
166
+ method: 'GET',
167
+ path: path(name, namespace: namespace),
168
+ response_class: @resource_class
169
+ )
170
+ end
171
+
172
+ # @param resource [resource_class]
173
+ # @return [Object] instance of resource_class
174
+ def get_resource(resource)
175
+ @transport.request(
176
+ method: 'GET',
177
+ path: path(resource.metadata.name, namespace: resource.metadata.namespace),
178
+ response_class: @resource_class
179
+ )
180
+ end
181
+
182
+ # @return [Bool]
183
+ def list?
184
+ @api_resource.verbs.include? 'list'
185
+ end
186
+
187
+ # @param list [K8s::API::MetaV1::List]
188
+ # @return [Array<Object>] array of instances of resource_class
189
+ def process_list(list)
190
+ list.items.map { |item|
191
+ # list items omit kind/apiVersion
192
+ @resource_class.new(item.merge('apiVersion' => list.apiVersion, 'kind' => @api_resource.kind))
193
+ }
194
+ end
195
+
196
+ # @param labelSelector [nil, String, Hash{String => String}]
197
+ # @param fieldSelector [nil, String, Hash{String => String}]
198
+ # @param namespace [nil, String]
199
+ # @return [Array<Object>] array of instances of resource_class
200
+ def list(labelSelector: nil, fieldSelector: nil, namespace: @namespace)
201
+ list = meta_list(labelSelector: labelSelector, fieldSelector: fieldSelector, namespace: namespace)
202
+ process_list(list)
203
+ end
204
+
205
+ # @param labelSelector [nil, String, Hash{String => String}]
206
+ # @param fieldSelector [nil, String, Hash{String => String}]
207
+ # @param namespace [nil, String]
208
+ # @return [K8s::API::MetaV1::List]
209
+ def meta_list(labelSelector: nil, fieldSelector: nil, namespace: @namespace)
210
+ @transport.request(
211
+ method: 'GET',
212
+ path: path(namespace: namespace),
213
+ response_class: K8s::API::MetaV1::List,
214
+ query: make_query(
215
+ 'labelSelector' => selector_query(labelSelector),
216
+ 'fieldSelector' => selector_query(fieldSelector)
217
+ )
218
+ )
219
+ end
220
+
221
+ # @param labelSelector [nil, String, Hash{String => String}]
222
+ # @param fieldSelector [nil, String, Hash{String => String}]
223
+ # @param resourceVersion [nil, String]
224
+ # @param timeout [nil, Integer]
225
+ # @yield [K8S::API::MetaV1::WatchEvent]
226
+ # @raise [Excon::Error]
227
+ def watch(labelSelector: nil, fieldSelector: nil, resourceVersion: nil, timeout: nil, namespace: @namespace)
228
+ method = 'GET'
229
+ path = path(namespace: namespace)
230
+ parser = Yajl::Parser.new
231
+ parser.on_parse_complete = lambda do |data|
232
+ event = K8s::API::MetaV1::WatchEvent.new(data)
233
+ yield event
234
+ end
235
+ @transport.request(
236
+ method: method,
237
+ path: path,
238
+ read_timeout: nil,
239
+ query: make_query(
240
+ 'labelSelector' => selector_query(labelSelector),
241
+ 'fieldSelector' => selector_query(fieldSelector),
242
+ 'resourceVersion' => resourceVersion,
243
+ 'watch' => '1',
244
+ 'timeoutSeconds' => timeout
245
+ ),
246
+ response_block: lambda do |chunk, _, _|
247
+ parser << chunk
248
+ end
249
+ )
250
+ end
251
+
252
+ # @return [Boolean]
253
+ def update?
254
+ @api_resource.verbs.include? 'update'
255
+ end
256
+
257
+ # @param resource [#metadata] with metadata.resourceVersion set
258
+ # @return [Object] instance of resource_class
259
+ def update_resource(resource)
260
+ @transport.request(
261
+ method: 'PUT',
262
+ path: path(resource.metadata.name, namespace: resource.metadata.namespace),
263
+ request_object: resource,
264
+ response_class: @resource_class
265
+ )
266
+ end
267
+
268
+ # @return [Boolean]
269
+ def patch?
270
+ @api_resource.verbs.include? 'patch'
271
+ end
272
+
273
+ # @param name [String]
274
+ # @param obj [#to_json]
275
+ # @param namespace [String, nil]
276
+ # @param strategic_merge [Boolean] use kube Strategic Merge Patch instead of standard Merge Patch (arrays of objects are merged by name)
277
+ # @return [Object] instance of resource_class
278
+ def merge_patch(name, obj, namespace: @namespace, strategic_merge: true)
279
+ @transport.request(
280
+ method: 'PATCH',
281
+ path: path(name, namespace: namespace),
282
+ content_type: strategic_merge ? 'application/strategic-merge-patch+json' : 'application/merge-patch+json',
283
+ request_object: obj,
284
+ response_class: @resource_class
285
+ )
286
+ end
287
+
288
+ # @param name [String]
289
+ # @param ops [Hash] json-patch operations
290
+ # @param namespace [String, nil]
291
+ # @return [Object] instance of resource_class
292
+ def json_patch(name, ops, namespace: @namespace)
293
+ @transport.request(
294
+ method: 'PATCH',
295
+ path: path(name, namespace: namespace),
296
+ content_type: 'application/json-patch+json',
297
+ request_object: ops,
298
+ response_class: @resource_class
299
+ )
300
+ end
301
+
302
+ # @return [Boolean]
303
+ def delete?
304
+ @api_resource.verbs.include? 'delete'
305
+ end
306
+
307
+ # @param name [String]
308
+ # @param namespace [String, nil]
309
+ # @param propagationPolicy [String, nil] The propagationPolicy to use for the API call. Possible values include “Orphan”, “Foreground”, or “Background”
310
+ # @return [K8s::API::MetaV1::Status]
311
+ def delete(name, namespace: @namespace, propagationPolicy: nil)
312
+ @transport.request(
313
+ method: 'DELETE',
314
+ path: path(name, namespace: namespace),
315
+ query: make_query(
316
+ 'propagationPolicy' => propagationPolicy
317
+ ),
318
+ response_class: @resource_class # XXX: documented as returning Status
319
+ )
320
+ end
321
+
322
+ # @param namespace [String, nil]
323
+ # @param labelSelector [nil, String, Hash{String => String}]
324
+ # @param fieldSelector [nil, String, Hash{String => String}]
325
+ # @param propagationPolicy [String, nil]
326
+ # @return [Array<Object>] array of instances of resource_class
327
+ def delete_collection(namespace: @namespace, labelSelector: nil, fieldSelector: nil, propagationPolicy: nil)
328
+ list = @transport.request(
329
+ method: 'DELETE',
330
+ path: path(namespace: namespace),
331
+ query: make_query(
332
+ 'labelSelector' => selector_query(labelSelector),
333
+ 'fieldSelector' => selector_query(fieldSelector),
334
+ 'propagationPolicy' => propagationPolicy
335
+ ),
336
+ response_class: K8s::API::MetaV1::List # XXX: documented as returning Status
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::API::MetaV1::Status]
345
+ def delete_resource(resource, **options)
346
+ delete(resource.metadata.name, namespace: resource.metadata.namespace, **options)
347
+ end
348
+ end
349
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module K8s
4
+ class Ruby
5
+ # Updated on releases using semver.
6
+ VERSION = "0.10.6"
7
+ end
8
+ end
data/lib/k8s/stack.rb ADDED
@@ -0,0 +1,187 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'securerandom'
4
+
5
+ module K8s
6
+ # Usage: customize the LABEL and CHECKSUM_ANNOTATION
7
+ class Stack
8
+ include Logging
9
+
10
+ # Label used to identify resources belonging to this stack
11
+ LABEL = 'k8s.kontena.io/stack'
12
+
13
+ # Annotation used to identify resource versions
14
+ CHECKSUM_ANNOTATION = 'k8s.kontena.io/stack-checksum'
15
+
16
+ # Annotation used to identify last applied configuration
17
+ LAST_CONFIG_ANNOTATION = 'kubectl.kubernetes.io/last-applied-configuration'
18
+
19
+ # List of apiVersion:kind combinations to skip for stack prune
20
+ # These would lead to stack prune misbehaving if not skipped.
21
+ PRUNE_IGNORE = [
22
+ 'v1:ComponentStatus', # apiserver ignores GET /v1/componentstatuses?labelSelector=... and returns all resources
23
+ 'v1:Endpoints' # inherits stack label from service, but not checksum annotation
24
+ ].freeze
25
+
26
+ # @param name [String] unique name for stack
27
+ # @param path [String] load resources from YAML files
28
+ # @param options [Hash] see Stack#initialize
29
+ # @return [K8s::Stack]
30
+ def self.load(name, path, **options)
31
+ resources = K8s::Resource.from_files(path)
32
+ new(name, resources, **options)
33
+ end
34
+
35
+ # @param name [String] unique name for stack
36
+ # @param path [String] load resources from YAML files
37
+ # @param client [K8s::Client] apply using client
38
+ # @param prune [Boolean] delete old resources
39
+ # @param options [Hash] see Stack#initialize
40
+ # @return [K8s::Stack]
41
+ def self.apply(name, path, client, prune: true, **options)
42
+ load(name, path, **options).apply(client, prune: prune)
43
+ end
44
+
45
+ # Remove any installed stack resources.
46
+ #
47
+ # @param name [String] unique name for stack
48
+ # @param client [K8s::Client] apply using client
49
+ def self.delete(name, client, **options)
50
+ new(name, **options).delete(client)
51
+ end
52
+
53
+ attr_reader :name, :resources
54
+
55
+ # @param name [String]
56
+ # @param resources [Array<K8s::Resource>]
57
+ # @param debug [Boolean]
58
+ # @param label [String]
59
+ # @param checksum_annotation [String]
60
+ # @param last_config_annotation [String]
61
+ def initialize(name, resources = [], debug: false, label: self.class::LABEL, checksum_annotation: self.class::CHECKSUM_ANNOTATION, last_configuration_annotation: self.class::LAST_CONFIG_ANNOTATION)
62
+ @name = name
63
+ @resources = resources
64
+ @keep_resources = {}
65
+ @label = label
66
+ @checksum_annotation = checksum_annotation
67
+ @last_config_annotation = last_configuration_annotation
68
+
69
+ logger! progname: name, debug: debug
70
+ end
71
+
72
+ # @param resource [K8s::Resource] to apply
73
+ # @param base_resource [K8s::Resource] DEPRECATED
74
+ # @return [K8s::Resource]
75
+ # rubocop:disable Lint/UnusedMethodArgument
76
+ def prepare_resource(resource, base_resource: nil)
77
+ # TODO: base_resource is not used anymore, kept for backwards compatibility for a while
78
+
79
+ # calculate checksum only from the "local" source
80
+ checksum = resource.checksum
81
+
82
+ # add stack metadata
83
+ resource.merge(
84
+ metadata: {
85
+ labels: { @label => name },
86
+ annotations: {
87
+ @checksum_annotation => checksum,
88
+ @last_config_annotation => Util.recursive_compact(resource.to_h).to_json
89
+ }
90
+ }
91
+ )
92
+ end
93
+ # rubocop:enable Lint/UnusedMethodArgument
94
+
95
+ # @param client [K8s::Client]
96
+ # @return [Array<K8s::Resource>]
97
+ def apply(client, prune: true, patch: true)
98
+ server_resources = client.get_resources(resources)
99
+
100
+ resources.zip(server_resources).map do |resource, server_resource|
101
+ if !server_resource
102
+ logger.info "Create resource #{resource.apiVersion}:#{resource.kind}/#{resource.metadata.name} in namespace #{resource.metadata.namespace} with checksum=#{resource.checksum}"
103
+ keep_resource! client.create_resource(prepare_resource(resource))
104
+ elsif server_resource.metadata.annotations&.dig(@checksum_annotation) != resource.checksum
105
+ logger.info "Update resource #{resource.apiVersion}:#{resource.kind}/#{resource.metadata.name} in namespace #{resource.metadata.namespace} with checksum=#{resource.checksum}"
106
+ r = prepare_resource(resource)
107
+ if patch && server_resource.can_patch?(@last_config_annotation)
108
+ # Patch: apply changes to specific fields only
109
+ keep_resource! client.patch_resource(server_resource, server_resource.merge_patch_ops(r.to_hash, @last_config_annotation))
110
+ else
111
+ # Replace (PUT): replace complete object
112
+ keep_resource! client.update_resource(server_resource.merge(prepare_resource(resource)))
113
+ end
114
+ else
115
+ logger.info "Keep resource #{server_resource.apiVersion}:#{server_resource.kind}/#{server_resource.metadata.name} in namespace #{server_resource.metadata.namespace} with checksum=#{server_resource.metadata.annotations[@checksum_annotation]}"
116
+ keep_resource! server_resource
117
+ end
118
+ end
119
+
120
+ prune(client, keep_resources: true) if prune
121
+ end
122
+
123
+ # key MUST NOT include resource.apiVersion: the same kind can be aliased in different APIs
124
+ # @param resource [K8s::Resource]
125
+ # @return [K8s::Resource]
126
+ def keep_resource!(resource)
127
+ @keep_resources["#{resource.kind}:#{resource.metadata.name}@#{resource.metadata.namespace}"] = resource.metadata.annotations[@checksum_annotation]
128
+ end
129
+
130
+ # @param resource [K8s::Resource]
131
+ # @return [Boolean]
132
+ def keep_resource?(resource)
133
+ keep_annotation = @keep_resources["#{resource.kind}:#{resource.metadata.name}@#{resource.metadata.namespace}"]
134
+ return false unless keep_annotation
135
+
136
+ keep_annotation == resource.metadata&.annotations.dig(@checksum_annotation)
137
+ end
138
+
139
+ # Delete all stack resources that were not applied
140
+ # @param client [K8s::Client]
141
+ # @param keep_resources [NilClass, Boolean]
142
+ # @param skip_forbidden [Boolean]
143
+ def prune(client, keep_resources:, skip_forbidden: true)
144
+ # using skip_forbidden: assume we can't create resource types that we are forbidden to list, so we don't need to prune them either
145
+ client.list_resources(labelSelector: { @label => name }, skip_forbidden: skip_forbidden).sort do |a, b|
146
+ # Sort resources so that namespaced objects are deleted first
147
+ if a.metadata.namespace == b.metadata.namespace
148
+ 0
149
+ elsif a.metadata.namespace.nil? && !b.metadata.namespace.nil?
150
+ 1
151
+ else
152
+ -1
153
+ end
154
+ end.each do |resource|
155
+ next if PRUNE_IGNORE.include? "#{resource.apiVersion}:#{resource.kind}"
156
+
157
+ resource_label = resource.metadata.labels ? resource.metadata.labels[@label] : nil
158
+ resource_checksum = resource.metadata.annotations ? resource.metadata.annotations[@checksum_annotation] : nil
159
+
160
+ logger.debug { "List resource #{resource.apiVersion}:#{resource.kind}/#{resource.metadata.name} in namespace #{resource.metadata.namespace} with checksum=#{resource_checksum}" }
161
+
162
+ if resource_label != name
163
+ # apiserver did not respect labelSelector
164
+ elsif resource.metadata&.ownerReferences && !resource.metadata.ownerReferences.empty?
165
+ logger.info "Server resource #{resource.apiVersion}:#{resource.apiKind}/#{resource.metadata.name} in namespace #{resource.metadata.namespace} has ownerReferences and will be kept"
166
+ elsif keep_resources && keep_resource?(resource)
167
+ # resource is up-to-date
168
+ else
169
+ logger.info "Delete resource #{resource.apiVersion}:#{resource.kind}/#{resource.metadata.name} in namespace #{resource.metadata.namespace}"
170
+ begin
171
+ client.delete_resource(resource, propagationPolicy: 'Background')
172
+ rescue K8s::Error::NotFound => e
173
+ # assume aliased objects in multiple API groups, like for Deployments
174
+ # alternatively, a custom resource whose definition was already deleted earlier
175
+ logger.debug { "Ignoring #{e} : #{e.message}" }
176
+ end
177
+ end
178
+ end
179
+ end
180
+
181
+ # Delete all stack resources
182
+ # @param client [K8s::Client]
183
+ def delete(client)
184
+ prune(client, keep_resources: false)
185
+ end
186
+ end
187
+ end