k8s-client 0.3.4 → 0.4.0
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 +4 -4
- data/k8s-client.gemspec +1 -0
- data/lib/k8s/client.rb +8 -2
- data/lib/k8s/client/version.rb +1 -1
- data/lib/k8s/resource.rb +29 -1
- data/lib/k8s/resource_client.rb +22 -3
- data/lib/k8s/stack.rb +29 -32
- data/lib/k8s/transport.rb +8 -1
- data/lib/k8s/util.rb +42 -0
- metadata +16 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ac23332f18be9e1b95502a9526eab9ed5e4f31a379540387c452802be09b61dc
|
4
|
+
data.tar.gz: 673e7eb930084bb9dd49353d87de7dc54631e8b4a8a85f3bb707c28573ca8441
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: f19573347f0f74af1e6eb9da2d8aa61d469aa6de69b2deef3938a39f74758191e5edc0f931d33e3953c3f3a96accb0463cfda704dec16d731b700a10e6f8fe7c
|
7
|
+
data.tar.gz: 426356558b0772b51324624ad06ee4ed3b6bef519fd78d845df5f020cf515f81ae1a4bc11febcae3609b425d46ca804645b17e53d891c52e26f9215d4586b4a4
|
data/k8s-client.gemspec
CHANGED
@@ -27,6 +27,7 @@ Gem::Specification.new do |spec|
|
|
27
27
|
spec.add_runtime_dependency "dry-struct", "~> 0.5.0"
|
28
28
|
spec.add_runtime_dependency "deep_merge", "~> 1.2.1"
|
29
29
|
spec.add_runtime_dependency "recursive-open-struct", "~> 1.1.0"
|
30
|
+
spec.add_runtime_dependency 'hashdiff', '~> 0.3.7'
|
30
31
|
|
31
32
|
spec.add_development_dependency "bundler", "~> 1.16"
|
32
33
|
spec.add_development_dependency "rake", "~> 10.0"
|
data/lib/k8s/client.rb
CHANGED
@@ -165,9 +165,15 @@ module K8s
|
|
165
165
|
end
|
166
166
|
|
167
167
|
# @param resource [K8s::Resource]
|
168
|
+
# @param options [Hash]
|
169
|
+
# @see ResourceClient#delete for options
|
168
170
|
# @return [K8s::Resource]
|
169
|
-
def delete_resource(resource)
|
170
|
-
client_for_resource(resource).delete_resource(resource)
|
171
|
+
def delete_resource(resource, **options)
|
172
|
+
client_for_resource(resource).delete_resource(resource, **options)
|
173
|
+
end
|
174
|
+
|
175
|
+
def patch_resource(resource, attrs)
|
176
|
+
client_for_resource(resource).json_patch(resource.metadata.name, attrs)
|
171
177
|
end
|
172
178
|
end
|
173
179
|
end
|
data/lib/k8s/client/version.rb
CHANGED
data/lib/k8s/resource.rb
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
require 'deep_merge'
|
2
2
|
require 'recursive-open-struct'
|
3
|
+
require 'hashdiff'
|
3
4
|
|
4
5
|
module K8s
|
5
6
|
# generic untyped resource
|
@@ -60,9 +61,36 @@ module K8s
|
|
60
61
|
h = to_hash
|
61
62
|
|
62
63
|
# merge in-place
|
63
|
-
h.deep_merge!(attrs.to_hash, overwrite_arrays: true)
|
64
|
+
h.deep_merge!(attrs.to_hash, overwrite_arrays: true, merge_nil_values: true)
|
64
65
|
|
65
66
|
self.class.new(h)
|
66
67
|
end
|
68
|
+
|
69
|
+
def checksum
|
70
|
+
@checksum ||= Digest::MD5.hexdigest(Marshal::dump(to_hash))
|
71
|
+
end
|
72
|
+
|
73
|
+
def merge_patch_ops(attrs, config_annotation)
|
74
|
+
Util.json_patch(current_config(config_annotation), stringify_hash(attrs))
|
75
|
+
end
|
76
|
+
|
77
|
+
# Gets the existing resources (on kube api) configuration, an empty hash if not present
|
78
|
+
#
|
79
|
+
# @return [Hash]
|
80
|
+
def current_config(config_annotation)
|
81
|
+
current_cfg = self.metadata.annotations&.dig(config_annotation)
|
82
|
+
|
83
|
+
return JSON.parse(current_cfg) if current_cfg
|
84
|
+
|
85
|
+
{}
|
86
|
+
end
|
87
|
+
|
88
|
+
def can_patch?(config_annotation)
|
89
|
+
!!self.metadata.annotations&.dig(config_annotation)
|
90
|
+
end
|
91
|
+
|
92
|
+
def stringify_hash(hash)
|
93
|
+
JSON.load(JSON.dump(hash))
|
94
|
+
end
|
67
95
|
end
|
68
96
|
end
|
data/lib/k8s/resource_client.rb
CHANGED
@@ -44,8 +44,9 @@ module K8s
|
|
44
44
|
# @param namespace [String, nil]
|
45
45
|
# @param labelSelector [nil, String, Hash{String => String}]
|
46
46
|
# @param fieldSelector [nil, String, Hash{String => String}]
|
47
|
+
# @param skip_forbidden [Boolean] skip resources that return HTTP 403 errors
|
47
48
|
# @return [Array<K8s::Resource>]
|
48
|
-
def self.list(resources, transport, namespace: nil, labelSelector: nil, fieldSelector: nil)
|
49
|
+
def self.list(resources, transport, namespace: nil, labelSelector: nil, fieldSelector: nil, skip_forbidden: false)
|
49
50
|
api_paths = resources.map{|resource| resource.path(namespace: namespace) }
|
50
51
|
api_lists = transport.gets(*api_paths,
|
51
52
|
response_class: K8s::API::MetaV1::List,
|
@@ -53,9 +54,10 @@ module K8s
|
|
53
54
|
'labelSelector' => selector_query(labelSelector),
|
54
55
|
'fieldSelector' => selector_query(fieldSelector),
|
55
56
|
),
|
57
|
+
skip_forbidden: skip_forbidden,
|
56
58
|
)
|
57
59
|
|
58
|
-
resources.zip(api_lists).map {|resource, api_list| resource.process_list(api_list) }.flatten
|
60
|
+
resources.zip(api_lists).map {|resource, api_list| api_list ? resource.process_list(api_list) : [] }.flatten
|
59
61
|
end
|
60
62
|
|
61
63
|
# @param transport [K8s::Transport]
|
@@ -243,6 +245,20 @@ module K8s
|
|
243
245
|
)
|
244
246
|
end
|
245
247
|
|
248
|
+
# @param name [String]
|
249
|
+
# @param ops [Hash] json-patch operations
|
250
|
+
# @param namespace [String]
|
251
|
+
# @return [resource_class]
|
252
|
+
def json_patch(name, ops, namespace: @namespace)
|
253
|
+
@transport.request(
|
254
|
+
method: 'PATCH',
|
255
|
+
path: self.path(name, namespace: namespace),
|
256
|
+
content_type: 'application/json-patch+json',
|
257
|
+
request_object: ops,
|
258
|
+
response_class: @resource_class,
|
259
|
+
)
|
260
|
+
end
|
261
|
+
|
246
262
|
# @return [Bool]
|
247
263
|
def delete?
|
248
264
|
@api_resource.verbs.include? 'delete'
|
@@ -250,13 +266,14 @@ module K8s
|
|
250
266
|
|
251
267
|
# @param name [String]
|
252
268
|
# @param namespace [String]
|
269
|
+
# @param propagationPolicy [String] The propagationPolicy to use for the API call. Possible values include “Orphan”, “Foreground”, or “Background”
|
253
270
|
# @return [K8s::API::MetaV1::Status]
|
254
271
|
def delete(name, namespace: @namespace, propagationPolicy: nil)
|
255
272
|
@transport.request(
|
256
273
|
method: 'DELETE',
|
257
274
|
path: self.path(name, namespace: namespace),
|
258
275
|
query: make_query(
|
259
|
-
'propagationPolicy' => propagationPolicy
|
276
|
+
'propagationPolicy' => propagationPolicy
|
260
277
|
),
|
261
278
|
response_class: @resource_class, # XXX: documented as returning Status
|
262
279
|
)
|
@@ -281,6 +298,8 @@ module K8s
|
|
281
298
|
end
|
282
299
|
|
283
300
|
# @param resource [resource_class] with metadata
|
301
|
+
# @param options [Hash]
|
302
|
+
# @see #delete for possible options
|
284
303
|
# @return [K8s::API::MetaV1::Status]
|
285
304
|
def delete_resource(resource, **options)
|
286
305
|
delete(resource.metadata.name, namespace: resource.metadata.namespace, **options)
|
data/lib/k8s/stack.rb
CHANGED
@@ -11,6 +11,9 @@ module K8s
|
|
11
11
|
# Annotation used to identify resource versions
|
12
12
|
CHECKSUM_ANNOTATION = 'k8s.kontena.io/stack-checksum'
|
13
13
|
|
14
|
+
# Annotation used to identify last applied configuration
|
15
|
+
LAST_CONFIG_ANNOTATION = 'kubectl.kubernetes.io/last-applied-configuration'
|
16
|
+
|
14
17
|
# List of apiVersion:kind combinations to skip for stack prune
|
15
18
|
# These would lead to stack prune misbehaving if not skipped.
|
16
19
|
PRUNE_IGNORE = [
|
@@ -44,37 +47,33 @@ module K8s
|
|
44
47
|
|
45
48
|
attr_reader :name, :resources
|
46
49
|
|
47
|
-
def initialize(name, resources = [], debug: false, label: self.class::LABEL, checksum_annotation: self.class::CHECKSUM_ANNOTATION)
|
50
|
+
def initialize(name, resources = [], debug: false, label: self.class::LABEL, checksum_annotation: self.class::CHECKSUM_ANNOTATION, last_configuration_annotation: self.class::LAST_CONFIG_ANNOTATION)
|
48
51
|
@name = name
|
49
52
|
@resources = resources
|
50
53
|
@keep_resources = {}
|
51
54
|
@label = label
|
52
55
|
@checksum_annotation = checksum_annotation
|
56
|
+
@last_config_annotation = last_configuration_annotation
|
53
57
|
|
54
58
|
logger! progname: name, debug: debug
|
55
59
|
end
|
56
60
|
|
57
|
-
# Random "checksum" used to identify different stack resource versions using an annotation.
|
58
|
-
#
|
59
|
-
# NOTE: This is not actually a checksum.
|
60
|
-
#
|
61
|
-
# @return [String]
|
62
|
-
def checksum
|
63
|
-
@checksum ||= SecureRandom.hex(16)
|
64
|
-
end
|
65
|
-
|
66
61
|
# @param resource [K8s::Resource] to apply
|
67
62
|
# @param base_resource [K8s::Resource] preserve existing attributes from base resource
|
68
63
|
# @return [K8s::Resource]
|
69
64
|
def prepare_resource(resource, base_resource: nil)
|
70
|
-
|
71
|
-
|
72
|
-
|
65
|
+
# XXX: base_resource is not really used anymore, kept for backwards compatibility for a while
|
66
|
+
|
67
|
+
# calculate checksum only from the "local" source
|
68
|
+
checksum = resource.checksum
|
73
69
|
|
74
70
|
# add stack metadata
|
75
71
|
resource.merge(metadata: {
|
76
72
|
labels: { @label => name },
|
77
|
-
annotations: {
|
73
|
+
annotations: {
|
74
|
+
@checksum_annotation => checksum,
|
75
|
+
@last_config_annotation => resource.to_json
|
76
|
+
},
|
78
77
|
})
|
79
78
|
end
|
80
79
|
|
@@ -83,24 +82,21 @@ module K8s
|
|
83
82
|
server_resources = client.get_resources(resources)
|
84
83
|
|
85
84
|
resources.zip(server_resources).map do |resource, server_resource|
|
86
|
-
if server_resource
|
87
|
-
# keep server checksum for comparison
|
88
|
-
# NOTE: this will not compare equal for resources with arrays containing hashes with default values applied by the server
|
89
|
-
# however, that will just cause extra PUTs, so it doesn't have any functional effects
|
90
|
-
compare_resource = server_resource.merge(resource).merge(metadata: {
|
91
|
-
labels: { @label => name },
|
92
|
-
})
|
93
|
-
end
|
94
|
-
|
95
85
|
if !server_resource
|
96
|
-
logger.info "Create resource #{resource.apiVersion}:#{resource.kind}/#{resource.metadata.name} in namespace #{resource.metadata.namespace} with checksum=#{checksum}"
|
86
|
+
logger.info "Create resource #{resource.apiVersion}:#{resource.kind}/#{resource.metadata.name} in namespace #{resource.metadata.namespace} with checksum=#{resource.checksum}"
|
97
87
|
keep_resource! client.create_resource(prepare_resource(resource))
|
98
|
-
elsif server_resource !=
|
99
|
-
logger.info "Update resource #{resource.apiVersion}:#{resource.kind}/#{resource.metadata.name} in namespace #{resource.metadata.namespace} with checksum=#{checksum}"
|
100
|
-
|
88
|
+
elsif server_resource.metadata.annotations[@checksum_annotation] != resource.checksum
|
89
|
+
logger.info "Update resource #{resource.apiVersion}:#{resource.kind}/#{resource.metadata.name} in namespace #{resource.metadata.namespace} with checksum=#{resource.checksum}"
|
90
|
+
r = prepare_resource(resource)
|
91
|
+
if server_resource.can_patch?(@last_config_annotation)
|
92
|
+
keep_resource! client.patch_resource(server_resource, server_resource.merge_patch_ops(r.to_hash, @last_config_annotation))
|
93
|
+
else
|
94
|
+
# try to update with PUT
|
95
|
+
keep_resource! client.update_resource(server_resource.merge(prepare_resource(resource)))
|
96
|
+
end
|
101
97
|
else
|
102
|
-
logger.info "Keep resource #{
|
103
|
-
keep_resource!
|
98
|
+
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]}"
|
99
|
+
keep_resource! server_resource
|
104
100
|
end
|
105
101
|
end
|
106
102
|
|
@@ -116,8 +112,9 @@ module K8s
|
|
116
112
|
end
|
117
113
|
|
118
114
|
# Delete all stack resources that were not applied
|
119
|
-
def prune(client, keep_resources: )
|
120
|
-
|
115
|
+
def prune(client, keep_resources: , skip_forbidden: true)
|
116
|
+
# 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
|
117
|
+
client.list_resources(labelSelector: {@label => name}, skip_forbidden: skip_forbidden).each do |resource|
|
121
118
|
next if PRUNE_IGNORE.include? "#{resource.apiVersion}:#{resource.kind}"
|
122
119
|
|
123
120
|
resource_label = resource.metadata.labels ? resource.metadata.labels[@label] : nil
|
@@ -132,7 +129,7 @@ module K8s
|
|
132
129
|
else
|
133
130
|
logger.info "Delete resource #{resource.apiVersion}:#{resource.kind}/#{resource.metadata.name} in namespace #{resource.metadata.namespace}"
|
134
131
|
begin
|
135
|
-
client.delete_resource(resource)
|
132
|
+
client.delete_resource(resource, propagationPolicy: 'Background')
|
136
133
|
rescue K8s::Error::NotFound
|
137
134
|
# assume aliased objects in multiple API groups, like for Deployments
|
138
135
|
# alternatively, a custom resource whose definition was already deleted earlier
|
data/lib/k8s/transport.rb
CHANGED
@@ -224,10 +224,11 @@ module K8s
|
|
224
224
|
|
225
225
|
# @param options [Array<Hash>] @see #request
|
226
226
|
# @param skip_missing [Boolean] return nil for HTTP 404 responses
|
227
|
+
# @param skip_forbidden [Boolean] return nil for HTTP 403 responses
|
227
228
|
# @param retry_errors [Boolean] retry with non-pipelined request for HTTP 503 responses
|
228
229
|
# @param common_options [Hash] @see #request, merged with the per-request options
|
229
230
|
# @return [Array<response_class, Hash, nil>]
|
230
|
-
def requests(*options, skip_missing: false, retry_errors: true, **common_options)
|
231
|
+
def requests(*options, skip_missing: false, skip_forbidden: false, retry_errors: true, **common_options)
|
231
232
|
return [] if options.empty? # excon chokes
|
232
233
|
|
233
234
|
start = Time.now
|
@@ -249,6 +250,12 @@ module K8s
|
|
249
250
|
else
|
250
251
|
raise
|
251
252
|
end
|
253
|
+
rescue K8s::Error::Forbidden
|
254
|
+
if skip_forbidden
|
255
|
+
nil
|
256
|
+
else
|
257
|
+
raise
|
258
|
+
end
|
252
259
|
rescue K8s::Error::ServiceUnavailable => exc
|
253
260
|
if retry_errors
|
254
261
|
logger.warn { "Retry #{format_request(request_options)} => HTTP #{exc.code} #{exc.reason} in #{'%.3f' % t}s" }
|
data/lib/k8s/util.rb
CHANGED
@@ -19,5 +19,47 @@ module K8s
|
|
19
19
|
|
20
20
|
args.map{|arg| value_map[arg] }
|
21
21
|
end
|
22
|
+
|
23
|
+
# Produces a set of json-patch operations so that applying
|
24
|
+
# the operations on a, gives you the results of b
|
25
|
+
# Used in correctly patching the Kube resources on stack updates
|
26
|
+
#
|
27
|
+
# @param a [Hash] Hash to compute patches against
|
28
|
+
# @param a [Hash] New Hash to compute patches "from"
|
29
|
+
def self.json_patch(a, b)
|
30
|
+
diffs = HashDiff.diff(a, b, array_path: true)
|
31
|
+
ops = []
|
32
|
+
# Each diff is like ["+", "spec.selector.aziz", "kebab"]
|
33
|
+
# or ["-", "spec.selector.aziz", "kebab"]
|
34
|
+
diffs.each do |diff|
|
35
|
+
operator = diff[0]
|
36
|
+
# substitute '/' with '~1' and '~' with '~0'
|
37
|
+
# according to RFC 6901
|
38
|
+
path = diff[1].map {|p| p.to_s.gsub('/', '~1')}.map {|p| p.to_s.gsub('~', '~0')}
|
39
|
+
if operator == '-'
|
40
|
+
ops << {
|
41
|
+
op: "remove",
|
42
|
+
path: "/" + path.join('/')
|
43
|
+
}
|
44
|
+
elsif operator == '+'
|
45
|
+
ops << {
|
46
|
+
op: "add",
|
47
|
+
path: "/" + path.join('/'),
|
48
|
+
value: diff[2]
|
49
|
+
}
|
50
|
+
elsif operator == '~'
|
51
|
+
ops << {
|
52
|
+
op: "replace",
|
53
|
+
path: "/" + path.join('/'),
|
54
|
+
value: diff[3]
|
55
|
+
}
|
56
|
+
else
|
57
|
+
raise "Unknown diff operator: #{operator}!"
|
58
|
+
end
|
59
|
+
|
60
|
+
end
|
61
|
+
|
62
|
+
ops
|
63
|
+
end
|
22
64
|
end
|
23
65
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: k8s-client
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.4.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Kontena, Inc.
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2018-
|
11
|
+
date: 2018-09-11 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: excon
|
@@ -66,6 +66,20 @@ dependencies:
|
|
66
66
|
- - "~>"
|
67
67
|
- !ruby/object:Gem::Version
|
68
68
|
version: 1.1.0
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: hashdiff
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: 0.3.7
|
76
|
+
type: :runtime
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: 0.3.7
|
69
83
|
- !ruby/object:Gem::Dependency
|
70
84
|
name: bundler
|
71
85
|
requirement: !ruby/object:Gem::Requirement
|