k8s-ruby 0.10.5
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/Dockerfile +11 -0
- data/Gemfile +6 -0
- data/LICENSE +201 -0
- data/README.md +214 -0
- data/Rakefile +16 -0
- data/bin/k8s-client +337 -0
- data/docker-compose.yaml +10 -0
- data/k8s-ruby.gemspec +40 -0
- data/lib/k8s-ruby.rb +4 -0
- data/lib/k8s/api.rb +31 -0
- data/lib/k8s/api/metav1.rb +26 -0
- data/lib/k8s/api/metav1/api_group.rb +26 -0
- data/lib/k8s/api/metav1/api_resource.rb +26 -0
- data/lib/k8s/api/metav1/list.rb +20 -0
- data/lib/k8s/api/metav1/object.rb +53 -0
- data/lib/k8s/api/metav1/status.rb +34 -0
- data/lib/k8s/api/metav1/watch_event.rb +20 -0
- data/lib/k8s/api/version.rb +17 -0
- data/lib/k8s/api_client.rb +116 -0
- data/lib/k8s/client.rb +283 -0
- data/lib/k8s/config.rb +208 -0
- data/lib/k8s/error.rb +66 -0
- data/lib/k8s/logging.rb +87 -0
- data/lib/k8s/resource.rb +117 -0
- data/lib/k8s/resource_client.rb +349 -0
- data/lib/k8s/ruby/version.rb +8 -0
- data/lib/k8s/stack.rb +186 -0
- data/lib/k8s/transport.rb +385 -0
- data/lib/k8s/util.rb +139 -0
- metadata +265 -0
@@ -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
|
data/lib/k8s/stack.rb
ADDED
@@ -0,0 +1,186 @@
|
|
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)
|
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 server_resource.can_patch?(@last_config_annotation)
|
108
|
+
keep_resource! client.patch_resource(server_resource, server_resource.merge_patch_ops(r.to_hash, @last_config_annotation))
|
109
|
+
else
|
110
|
+
# try to update with PUT
|
111
|
+
keep_resource! client.update_resource(server_resource.merge(prepare_resource(resource)))
|
112
|
+
end
|
113
|
+
else
|
114
|
+
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]}"
|
115
|
+
keep_resource! server_resource
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
prune(client, keep_resources: true) if prune
|
120
|
+
end
|
121
|
+
|
122
|
+
# key MUST NOT include resource.apiVersion: the same kind can be aliased in different APIs
|
123
|
+
# @param resource [K8s::Resource]
|
124
|
+
# @return [K8s::Resource]
|
125
|
+
def keep_resource!(resource)
|
126
|
+
@keep_resources["#{resource.kind}:#{resource.metadata.name}@#{resource.metadata.namespace}"] = resource.metadata.annotations[@checksum_annotation]
|
127
|
+
end
|
128
|
+
|
129
|
+
# @param resource [K8s::Resource]
|
130
|
+
# @return [Boolean]
|
131
|
+
def keep_resource?(resource)
|
132
|
+
keep_annotation = @keep_resources["#{resource.kind}:#{resource.metadata.name}@#{resource.metadata.namespace}"]
|
133
|
+
return false unless keep_annotation
|
134
|
+
|
135
|
+
keep_annotation == resource.metadata&.annotations.dig(@checksum_annotation)
|
136
|
+
end
|
137
|
+
|
138
|
+
# Delete all stack resources that were not applied
|
139
|
+
# @param client [K8s::Client]
|
140
|
+
# @param keep_resources [NilClass, Boolean]
|
141
|
+
# @param skip_forbidden [Boolean]
|
142
|
+
def prune(client, keep_resources:, skip_forbidden: true)
|
143
|
+
# 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
|
144
|
+
client.list_resources(labelSelector: { @label => name }, skip_forbidden: skip_forbidden).sort do |a, b|
|
145
|
+
# Sort resources so that namespaced objects are deleted first
|
146
|
+
if a.metadata.namespace == b.metadata.namespace
|
147
|
+
0
|
148
|
+
elsif a.metadata.namespace.nil? && !b.metadata.namespace.nil?
|
149
|
+
1
|
150
|
+
else
|
151
|
+
-1
|
152
|
+
end
|
153
|
+
end.each do |resource|
|
154
|
+
next if PRUNE_IGNORE.include? "#{resource.apiVersion}:#{resource.kind}"
|
155
|
+
|
156
|
+
resource_label = resource.metadata.labels ? resource.metadata.labels[@label] : nil
|
157
|
+
resource_checksum = resource.metadata.annotations ? resource.metadata.annotations[@checksum_annotation] : nil
|
158
|
+
|
159
|
+
logger.debug { "List resource #{resource.apiVersion}:#{resource.kind}/#{resource.metadata.name} in namespace #{resource.metadata.namespace} with checksum=#{resource_checksum}" }
|
160
|
+
|
161
|
+
if resource_label != name
|
162
|
+
# apiserver did not respect labelSelector
|
163
|
+
elsif resource.metadata&.ownerReferences && !resource.metadata.ownerReferences.empty?
|
164
|
+
logger.info "Server resource #{resource.apiVersion}:#{resource.apiKind}/#{resource.metadata.name} in namespace #{resource.metadata.namespace} has ownerReferences and will be kept"
|
165
|
+
elsif keep_resources && keep_resource?(resource)
|
166
|
+
# resource is up-to-date
|
167
|
+
else
|
168
|
+
logger.info "Delete resource #{resource.apiVersion}:#{resource.kind}/#{resource.metadata.name} in namespace #{resource.metadata.namespace}"
|
169
|
+
begin
|
170
|
+
client.delete_resource(resource, propagationPolicy: 'Background')
|
171
|
+
rescue K8s::Error::NotFound => e
|
172
|
+
# assume aliased objects in multiple API groups, like for Deployments
|
173
|
+
# alternatively, a custom resource whose definition was already deleted earlier
|
174
|
+
logger.debug { "Ignoring #{e} : #{e.message}" }
|
175
|
+
end
|
176
|
+
end
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
# Delete all stack resources
|
181
|
+
# @param client [K8s::Client]
|
182
|
+
def delete(client)
|
183
|
+
prune(client, keep_resources: false)
|
184
|
+
end
|
185
|
+
end
|
186
|
+
end
|