k8s-client 0.4.2 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
data/lib/k8s/logging.rb CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'logger'
2
4
 
3
5
  module K8s
@@ -51,8 +53,8 @@ module K8s
51
53
  # @return [Logger]
52
54
  def logger(target: LOG_TARGET, level: nil)
53
55
  @logger ||= Logger.new(target).tap do |logger|
54
- logger.progname = self.name
55
- logger.level = level || self.log_level || K8s::Logging.log_level || LOG_LEVEL
56
+ logger.progname = name
57
+ logger.level = level || log_level || K8s::Logging.log_level || LOG_LEVEL
56
58
  end
57
59
  end
58
60
  end
data/lib/k8s/resource.rb CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'deep_merge'
2
4
  require 'recursive-open-struct'
3
5
  require 'hashdiff'
@@ -11,7 +13,7 @@ module K8s
11
13
  # @param data [Hash]
12
14
  # @return [self]
13
15
  def self.from_json(data)
14
- return new(data)
16
+ new(data)
15
17
  end
16
18
 
17
19
  # @param filename [String] file path
@@ -27,9 +29,9 @@ module K8s
27
29
 
28
30
  if stat.directory?
29
31
  # recurse
30
- Dir.glob("#{path}/*.{yml,yaml}").sort.map { |dir| self.from_files(dir) }.flatten
32
+ Dir.glob("#{path}/*.{yml,yaml}").sort.map { |dir| from_files(dir) }.flatten
31
33
  else
32
- ::YAML.load_stream(File.read(path), path).map{|doc| new(doc) }
34
+ ::YAML.load_stream(File.read(path), path).map{ |doc| new(doc) }
33
35
  end
34
36
  end
35
37
 
@@ -67,7 +69,7 @@ module K8s
67
69
  end
68
70
 
69
71
  def checksum
70
- @checksum ||= Digest::MD5.hexdigest(Marshal::dump(to_hash))
72
+ @checksum ||= Digest::MD5.hexdigest(Marshal.dump(to_hash))
71
73
  end
72
74
 
73
75
  def merge_patch_ops(attrs, config_annotation)
@@ -78,7 +80,7 @@ module K8s
78
80
  #
79
81
  # @return [Hash]
80
82
  def current_config(config_annotation)
81
- current_cfg = self.metadata.annotations&.dig(config_annotation)
83
+ current_cfg = metadata.annotations&.dig(config_annotation)
82
84
  return {} unless current_cfg
83
85
 
84
86
  current_hash = JSON.parse(current_cfg)
@@ -89,11 +91,11 @@ module K8s
89
91
  end
90
92
 
91
93
  def can_patch?(config_annotation)
92
- !!self.metadata.annotations&.dig(config_annotation)
94
+ !!metadata.annotations&.dig(config_annotation)
93
95
  end
94
96
 
95
97
  def stringify_hash(hash)
96
- JSON.load(JSON.dump(hash))
98
+ JSON.parse(JSON.dump(hash))
97
99
  end
98
100
  end
99
101
  end
@@ -1,9 +1,10 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module K8s
2
4
  # Per-APIResource type client.
3
5
  #
4
6
  # Used to get/list/update/patch/delete specific types of resources, optionally in some specific namespace.
5
7
  class ResourceClient
6
-
7
8
  # Common helpers used in both class/instance methods
8
9
  module Utils
9
10
  # @param selector [nil, String, Hash{String => String}]
@@ -15,7 +16,7 @@ module K8s
15
16
  when String
16
17
  selector
17
18
  when Hash
18
- selector.map{|k, v| "#{k}=#{v}"}.join ','
19
+ selector.map{ |k, v| "#{k}=#{v}" }.join ','
19
20
  else
20
21
  fail "Invalid selector type. #{selector.inspect}"
21
22
  end
@@ -47,17 +48,18 @@ module K8s
47
48
  # @param skip_forbidden [Boolean] skip resources that return HTTP 403 errors
48
49
  # @return [Array<K8s::Resource>]
49
50
  def self.list(resources, transport, namespace: nil, labelSelector: nil, fieldSelector: nil, skip_forbidden: false)
50
- api_paths = resources.map{|resource| resource.path(namespace: namespace) }
51
- api_lists = transport.gets(*api_paths,
52
- response_class: K8s::API::MetaV1::List,
53
- query: make_query(
54
- 'labelSelector' => selector_query(labelSelector),
55
- 'fieldSelector' => selector_query(fieldSelector),
56
- ),
57
- skip_forbidden: skip_forbidden,
58
- )
59
-
60
- resources.zip(api_lists).map {|resource, api_list| api_list ? resource.process_list(api_list) : [] }.flatten
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
61
63
  end
62
64
 
63
65
  # @param transport [K8s::Transport]
@@ -78,7 +80,7 @@ module K8s
78
80
  @subresource = nil
79
81
  end
80
82
 
81
- fail "Resource #{api_resource.name} is not namespaced" if namespace unless api_resource.namespaced
83
+ fail "Resource #{api_resource.name} is not namespaced" unless api_resource.namespaced || !namespace
82
84
  end
83
85
 
84
86
  # @return [String]
@@ -92,14 +94,10 @@ module K8s
92
94
  end
93
95
 
94
96
  # @return [String, nil]
95
- def namespace
96
- @namespace
97
- end
97
+ attr_reader :namespace
98
98
 
99
99
  # @return [String]
100
- def resource
101
- @resource
102
- end
100
+ attr_reader :resource
103
101
 
104
102
  # @return [Boolean]
105
103
  def subresource?
@@ -107,9 +105,7 @@ module K8s
107
105
  end
108
106
 
109
107
  # @return [String, nil]
110
- def subresource
111
- @subresource
112
- end
108
+ attr_reader :subresource
113
109
 
114
110
  # @return [String]
115
111
  def kind
@@ -117,9 +113,7 @@ module K8s
117
113
  end
118
114
 
119
115
  # @return [class] K8s::Resource
120
- def resource_class
121
- @resource_class
122
- end
116
+ attr_reader :resource_class
123
117
 
124
118
  # @return [Bool]
125
119
  def namespaced?
@@ -149,9 +143,9 @@ module K8s
149
143
  def create_resource(resource)
150
144
  @transport.request(
151
145
  method: 'POST',
152
- path: self.path(namespace: resource.metadata.namespace),
146
+ path: path(namespace: resource.metadata.namespace),
153
147
  request_object: resource,
154
- response_class: @resource_class,
148
+ response_class: @resource_class
155
149
  )
156
150
  end
157
151
 
@@ -164,8 +158,8 @@ module K8s
164
158
  def get(name, namespace: @namespace)
165
159
  @transport.request(
166
160
  method: 'GET',
167
- path: self.path(name, namespace: namespace),
168
- response_class: @resource_class,
161
+ path: path(name, namespace: namespace),
162
+ response_class: @resource_class
169
163
  )
170
164
  end
171
165
 
@@ -174,8 +168,8 @@ module K8s
174
168
  def get_resource(resource)
175
169
  @transport.request(
176
170
  method: 'GET',
177
- path: self.path(resource.metadata.name, namespace: resource.metadata.namespace),
178
- response_class: @resource_class,
171
+ path: path(resource.metadata.name, namespace: resource.metadata.namespace),
172
+ response_class: @resource_class
179
173
  )
180
174
  end
181
175
 
@@ -187,7 +181,7 @@ module K8s
187
181
  # @param list [K8s::API::MetaV1::List]
188
182
  # @return [Array<resource_class>]
189
183
  def process_list(list)
190
- list.items.map {|item|
184
+ list.items.map { |item|
191
185
  # list items omit kind/apiVersion
192
186
  @resource_class.new(item.merge('apiVersion' => list.apiVersion, 'kind' => @api_resource.kind))
193
187
  }
@@ -199,12 +193,12 @@ module K8s
199
193
  def list(labelSelector: nil, fieldSelector: nil, namespace: @namespace)
200
194
  list = @transport.request(
201
195
  method: 'GET',
202
- path: self.path(namespace: namespace),
196
+ path: path(namespace: namespace),
203
197
  response_class: K8s::API::MetaV1::List,
204
198
  query: make_query(
205
199
  'labelSelector' => selector_query(labelSelector),
206
- 'fieldSelector' => selector_query(fieldSelector),
207
- ),
200
+ 'fieldSelector' => selector_query(fieldSelector)
201
+ )
208
202
  )
209
203
  process_list(list)
210
204
  end
@@ -219,9 +213,9 @@ module K8s
219
213
  def update_resource(resource)
220
214
  @transport.request(
221
215
  method: 'PUT',
222
- path: self.path(resource.metadata.name, namespace: resource.metadata.namespace),
216
+ path: path(resource.metadata.name, namespace: resource.metadata.namespace),
223
217
  request_object: resource,
224
- response_class: @resource_class,
218
+ response_class: @resource_class
225
219
  )
226
220
  end
227
221
 
@@ -238,10 +232,10 @@ module K8s
238
232
  def merge_patch(name, obj, namespace: @namespace, strategic_merge: true)
239
233
  @transport.request(
240
234
  method: 'PATCH',
241
- path: self.path(name, namespace: namespace),
235
+ path: path(name, namespace: namespace),
242
236
  content_type: strategic_merge ? 'application/strategic-merge-patch+json' : 'application/merge-patch+json',
243
237
  request_object: obj,
244
- response_class: @resource_class,
238
+ response_class: @resource_class
245
239
  )
246
240
  end
247
241
 
@@ -252,10 +246,10 @@ module K8s
252
246
  def json_patch(name, ops, namespace: @namespace)
253
247
  @transport.request(
254
248
  method: 'PATCH',
255
- path: self.path(name, namespace: namespace),
249
+ path: path(name, namespace: namespace),
256
250
  content_type: 'application/json-patch+json',
257
251
  request_object: ops,
258
- response_class: @resource_class,
252
+ response_class: @resource_class
259
253
  )
260
254
  end
261
255
 
@@ -271,7 +265,7 @@ module K8s
271
265
  def delete(name, namespace: @namespace, propagationPolicy: nil)
272
266
  @transport.request(
273
267
  method: 'DELETE',
274
- path: self.path(name, namespace: namespace),
268
+ path: path(name, namespace: namespace),
275
269
  query: make_query(
276
270
  'propagationPolicy' => propagationPolicy
277
271
  ),
@@ -286,11 +280,11 @@ module K8s
286
280
  def delete_collection(namespace: @namespace, labelSelector: nil, fieldSelector: nil, propagationPolicy: nil)
287
281
  list = @transport.request(
288
282
  method: 'DELETE',
289
- path: self.path(namespace: namespace),
283
+ path: path(namespace: namespace),
290
284
  query: make_query(
291
285
  'labelSelector' => selector_query(labelSelector),
292
286
  'fieldSelector' => selector_query(fieldSelector),
293
- 'propagationPolicy' => propagationPolicy,
287
+ 'propagationPolicy' => propagationPolicy
294
288
  ),
295
289
  response_class: K8s::API::MetaV1::List, # XXX: documented as returning Status
296
290
  )
data/lib/k8s/stack.rb CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'securerandom'
2
4
 
3
5
  module K8s
@@ -19,7 +21,7 @@ module K8s
19
21
  PRUNE_IGNORE = [
20
22
  'v1:ComponentStatus', # apiserver ignores GET /v1/componentstatuses?labelSelector=... and returns all resources
21
23
  'v1:Endpoints', # inherits stack label from service, but not checksum annotation
22
- ]
24
+ ].freeze
23
25
 
24
26
  # @param name [String] unique name for stack
25
27
  # @param path [String] load resources from YAML files
@@ -59,23 +61,27 @@ module K8s
59
61
  end
60
62
 
61
63
  # @param resource [K8s::Resource] to apply
62
- # @param base_resource [K8s::Resource] preserve existing attributes from base resource
64
+ # @param base_resource [K8s::Resource] DEPRECATED
63
65
  # @return [K8s::Resource]
66
+ # rubocop:disable Lint/UnusedMethodArgument
64
67
  def prepare_resource(resource, base_resource: nil)
65
- # XXX: base_resource is not really used anymore, kept for backwards compatibility for a while
68
+ # TODO: base_resource is not used anymore, kept for backwards compatibility for a while
66
69
 
67
70
  # calculate checksum only from the "local" source
68
71
  checksum = resource.checksum
69
72
 
70
73
  # add stack metadata
71
- resource.merge(metadata: {
72
- labels: { @label => name },
73
- annotations: {
74
- @checksum_annotation => checksum,
75
- @last_config_annotation => resource.to_json
76
- },
77
- })
74
+ resource.merge(
75
+ metadata: {
76
+ labels: { @label => name },
77
+ annotations: {
78
+ @checksum_annotation => checksum,
79
+ @last_config_annotation => resource.to_json
80
+ }
81
+ }
82
+ )
78
83
  end
84
+ # rubocop:enable Lint/UnusedMethodArgument
79
85
 
80
86
  # @return [Array<K8s::Resource>]
81
87
  def apply(client, prune: true)
@@ -107,14 +113,15 @@ module K8s
107
113
  def keep_resource!(resource)
108
114
  @keep_resources["#{resource.kind}:#{resource.metadata.name}@#{resource.metadata.namespace}"] = resource.metadata.annotations[@checksum_annotation]
109
115
  end
116
+
110
117
  def keep_resource?(resource)
111
118
  @keep_resources["#{resource.kind}:#{resource.metadata.name}@#{resource.metadata.namespace}"] == resource.metadata.annotations[@checksum_annotation]
112
119
  end
113
120
 
114
121
  # Delete all stack resources that were not applied
115
- def prune(client, keep_resources: , skip_forbidden: true)
122
+ def prune(client, keep_resources:, skip_forbidden: true)
116
123
  # 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).sort{ |a,b|
124
+ client.list_resources(labelSelector: { @label => name }, skip_forbidden: skip_forbidden).sort do |a, b|
118
125
  # Sort resources so that namespaced objects are deleted first
119
126
  if a.metadata.namespace == b.metadata.namespace
120
127
  0
@@ -123,7 +130,7 @@ module K8s
123
130
  else
124
131
  -1
125
132
  end
126
- }.each do |resource|
133
+ end.each do |resource|
127
134
  next if PRUNE_IGNORE.include? "#{resource.apiVersion}:#{resource.kind}"
128
135
 
129
136
  resource_label = resource.metadata.labels ? resource.metadata.labels[@label] : nil
@@ -139,9 +146,10 @@ module K8s
139
146
  logger.info "Delete resource #{resource.apiVersion}:#{resource.kind}/#{resource.metadata.name} in namespace #{resource.metadata.namespace}"
140
147
  begin
141
148
  client.delete_resource(resource, propagationPolicy: 'Background')
142
- rescue K8s::Error::NotFound
149
+ rescue K8s::Error::NotFound => ex
143
150
  # assume aliased objects in multiple API groups, like for Deployments
144
151
  # alternatively, a custom resource whose definition was already deleted earlier
152
+ logger.debug { "Ignoring #{ex} : #{ex.message}" }
145
153
  end
146
154
  end
147
155
  end
data/lib/k8s/transport.rb CHANGED
@@ -1,5 +1,8 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'excon'
2
4
  require 'json'
5
+ require 'jsonpath'
3
6
 
4
7
  module K8s
5
8
  # Excon-based HTTP transport handling request/response body JSON encoding
@@ -11,13 +14,13 @@ module K8s
11
14
  # Excon middlewares for requests
12
15
  EXCON_MIDDLEWARES = [
13
16
  # XXX: necessary? redirected requests omit authz headers?
14
- Excon::Middleware::RedirectFollower,
17
+ Excon::Middleware::RedirectFollower
15
18
  ] + Excon.defaults[:middlewares]
16
19
 
17
20
  # Default request headers
18
21
  REQUEST_HEADERS = {
19
- 'Accept' => 'application/json',
20
- }
22
+ 'Accept' => 'application/json'
23
+ }.freeze
21
24
 
22
25
  # Construct transport from kubeconfig
23
26
  #
@@ -67,6 +70,16 @@ module K8s
67
70
  logger.debug "Using config with .user.token=..."
68
71
 
69
72
  options[:auth_token] = token
73
+ elsif config.user.auth_provider && auth_provider = config.user.auth_provider.config
74
+ logger.debug "Using config with .user.auth-provider.name=#{config.user.auth_provider.name}"
75
+
76
+ auth_data = `#{auth_provider['cmd-path']} #{auth_provider['cmd-args']}`.strip
77
+ if auth_provider['token-key']
78
+ json_path = JsonPath.new(auth_provider['token-key'][1...-1])
79
+ options[:auth_token] = json_path.first(auth_data)
80
+ else
81
+ options[:auth_token] = auth_data
82
+ end
70
83
  end
71
84
 
72
85
  logger.info "Using config with server=#{server}"
@@ -81,10 +94,11 @@ module K8s
81
94
  host = ENV['KUBERNETES_SERVICE_HOST']
82
95
  port = ENV['KUBERNETES_SERVICE_PORT_HTTPS']
83
96
 
84
- new("https://#{host}:#{port}",
97
+ new(
98
+ "https://#{host}:#{port}",
85
99
  ssl_verify_peer: true,
86
100
  ssl_ca_file: '/var/run/secrets/kubernetes.io/serviceaccount/ca.crt',
87
- auth_token: File.read('/var/run/secrets/kubernetes.io/serviceaccount/token'),
101
+ auth_token: File.read('/var/run/secrets/kubernetes.io/serviceaccount/token')
88
102
  )
89
103
  end
90
104
 
@@ -103,7 +117,8 @@ module K8s
103
117
 
104
118
  # @return [Excon::Connection]
105
119
  def excon
106
- @excon ||= Excon.new(@server,
120
+ @excon ||= Excon.new(
121
+ @server,
107
122
  persistent: true,
108
123
  middlewares: EXCON_MIDDLEWARES,
109
124
  headers: REQUEST_HEADERS,
@@ -145,6 +160,7 @@ module K8s
145
160
  if options[:query]
146
161
  path += Excon::Utils.query_string(options)
147
162
  end
163
+
148
164
  if obj = options[:request_object]
149
165
  body = "<#{obj.class.name}>"
150
166
  end
@@ -178,11 +194,9 @@ module K8s
178
194
  raise K8s::Error::API.new(method, path, response.status, "Invalid JSON response: #{response_data.inspect}")
179
195
  end
180
196
 
181
- if response_class
182
- return response_class.from_json(response_data)
183
- else
184
- return response_data # Hash
185
- end
197
+ return response_data unless response_class
198
+
199
+ response_class.from_json(response_data)
186
200
  else
187
201
  error_class = K8s::Error::HTTP_STATUS_ERRORS[response.status] || K8s::Error::API
188
202
 
@@ -208,18 +222,17 @@ module K8s
208
222
  t = Time.now - start
209
223
 
210
224
  obj = parse_response(response, options,
211
- response_class: response_class,
212
- )
225
+ response_class: response_class)
213
226
  rescue K8s::Error::API => exc
214
- logger.warn { "#{format_request(options)} => HTTP #{exc.code} #{exc.reason} in #{'%.3f' % t}s"}
215
- logger.debug { "Request: #{excon_options[:body]}"} if excon_options[:body]
216
- logger.debug { "Response: #{response.body}"}
227
+ logger.warn { "#{format_request(options)} => HTTP #{exc.code} #{exc.reason} in #{'%.3f' % t}s" }
228
+ logger.debug { "Request: #{excon_options[:body]}" } if excon_options[:body]
229
+ logger.debug { "Response: #{response.body}" }
217
230
  raise
218
231
  else
219
- logger.info { "#{format_request(options)} => HTTP #{response.status}: <#{obj.class}> in #{'%.3f' % t}s"}
220
- logger.debug { "Request: #{excon_options[:body]}"} if excon_options[:body]
221
- logger.debug { "Response: #{response.body}"}
222
- return obj
232
+ logger.info { "#{format_request(options)} => HTTP #{response.status}: <#{obj.class}> in #{'%.3f' % t}s" }
233
+ logger.debug { "Request: #{excon_options[:body]}" } if excon_options[:body]
234
+ logger.debug { "Response: #{response.body}" }
235
+ obj
223
236
  end
224
237
 
225
238
  # @param options [Array<Hash>] @see #request
@@ -233,46 +246,39 @@ module K8s
233
246
 
234
247
  start = Time.now
235
248
  responses = excon.requests(
236
- options.map{|options| request_options(**common_options.merge(options))}
249
+ options.map{ |opts| request_options(**common_options.merge(opts)) }
237
250
  )
238
251
  t = Time.now - start
239
252
 
240
- objects = responses.zip(options).map{|response, request_options|
253
+ objects = responses.zip(options).map{ |response, request_options|
241
254
  response_class = request_options[:response_class] || common_options[:response_class]
242
255
 
243
256
  begin
244
257
  parse_response(response, request_options,
245
- response_class: response_class,
246
- )
258
+ response_class: response_class)
247
259
  rescue K8s::Error::NotFound
248
- if skip_missing
249
- nil
250
- else
251
- raise
252
- end
260
+ raise unless skip_missing
261
+
262
+ nil
253
263
  rescue K8s::Error::Forbidden
254
- if skip_forbidden
255
- nil
256
- else
257
- raise
258
- end
264
+ raise unless skip_forbidden
265
+
266
+ nil
259
267
  rescue K8s::Error::ServiceUnavailable => exc
260
- if retry_errors
261
- logger.warn { "Retry #{format_request(request_options)} => HTTP #{exc.code} #{exc.reason} in #{'%.3f' % t}s" }
262
-
263
- # only retry the failed request, not the entire pipeline
264
- request(response_class: response_class, **common_options.merge(request_options))
265
- else
266
- raise
267
- end
268
+ raise unless retry_errors
269
+
270
+ logger.warn { "Retry #{format_request(request_options)} => HTTP #{exc.code} #{exc.reason} in #{'%.3f' % t}s" }
271
+
272
+ # only retry the failed request, not the entire pipeline
273
+ request(response_class: response_class, **common_options.merge(request_options))
268
274
  end
269
275
  }
270
276
  rescue K8s::Error => exc
271
- logger.warn { "[#{options.map{|o| format_request(o)}.join ', '}] => HTTP #{exc.code} #{exc.reason} in #{'%.3f' % t}s"}
277
+ logger.warn { "[#{options.map{ |o| format_request(o) }.join ', '}] => HTTP #{exc.code} #{exc.reason} in #{'%.3f' % t}s" }
272
278
  raise
273
279
  else
274
- logger.info { "[#{options.map{|o| format_request(o)}.join ', '}] => HTTP [#{responses.map{|r| r.status}.join ', '}] in #{'%.3f' % t}s" }
275
- return objects
280
+ logger.info { "[#{options.map{ |o| format_request(o) }.join ', '}] => HTTP [#{responses.map(&:status).join ', '}] in #{'%.3f' % t}s" }
281
+ objects
276
282
  end
277
283
 
278
284
  # @param path [Array<String>] @see #path
@@ -281,17 +287,20 @@ module K8s
281
287
  request(
282
288
  method: 'GET',
283
289
  path: self.path(*path),
284
- **options,
290
+ **options
285
291
  )
286
292
  end
287
293
 
288
294
  # @param paths [Array<String>]
289
295
  # @param options [Hash] @see #request
290
296
  def gets(*paths, **options)
291
- requests(*paths.map{|path| {
292
- method: 'GET',
293
- path: self.path(path),
294
- } },
297
+ requests(
298
+ *paths.map do |path|
299
+ {
300
+ method: 'GET',
301
+ path: self.path(path)
302
+ }
303
+ end,
295
304
  **options
296
305
  )
297
306
  end