k8s-client 0.3.4 → 0.4.0

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