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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 629b1fd35aba3aa4999fb320033c537e239768a3ea2a9163b19a2ba69f00466d
4
- data.tar.gz: 35c3043b726d29a0fb038433b4764fdd8de9b2665b1db8c6a7649c6403f9e98d
3
+ metadata.gz: ac23332f18be9e1b95502a9526eab9ed5e4f31a379540387c452802be09b61dc
4
+ data.tar.gz: 673e7eb930084bb9dd49353d87de7dc54631e8b4a8a85f3bb707c28573ca8441
5
5
  SHA512:
6
- metadata.gz: cac2f739ee3dcfe3885fe4070c4497e22cc86d227a449c18c773390299e5e416458971a2d424abbcae52b163f4077fc8d85138865c1f1857ca3a83f27dae9e84
7
- data.tar.gz: a2bd611f565e0f6e3e9f54d201f89bc2a5eef307b9d3ce7327c267139fd29744a7f721d655f6b67b9aa4126db0c97830b1e2a8330f5abde8009f3ac954c6e7f4
6
+ metadata.gz: f19573347f0f74af1e6eb9da2d8aa61d469aa6de69b2deef3938a39f74758191e5edc0f931d33e3953c3f3a96accb0463cfda704dec16d731b700a10e6f8fe7c
7
+ data.tar.gz: 426356558b0772b51324624ad06ee4ed3b6bef519fd78d845df5f020cf515f81ae1a4bc11febcae3609b425d46ca804645b17e53d891c52e26f9215d4586b4a4
@@ -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"
@@ -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
@@ -1,6 +1,6 @@
1
1
  module K8s
2
2
  class Client
3
3
  # Updated on releases using semver.
4
- VERSION = "0.3.4"
4
+ VERSION = "0.4.0"
5
5
  end
6
6
  end
@@ -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
@@ -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)
@@ -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
- if base_resource
71
- resource = base_resource.merge(resource)
72
- end
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: { @checksum_annotation => checksum },
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 != compare_resource
99
- logger.info "Update resource #{resource.apiVersion}:#{resource.kind}/#{resource.metadata.name} in namespace #{resource.metadata.namespace} with checksum=#{checksum}"
100
- keep_resource! client.update_resource(prepare_resource(resource, base_resource: server_resource))
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 #{resource.apiVersion}:#{resource.kind}/#{resource.metadata.name} in namespace #{resource.metadata.namespace} with checksum=#{compare_resource.metadata.annotations[@checksum_annotation]}"
103
- keep_resource! compare_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
- client.list_resources(labelSelector: {@label => name}).each do |resource|
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
@@ -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" }
@@ -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.3.4
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-08-21 00:00:00.000000000 Z
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