etna 0.1.47 → 0.1.48

Sign up to get free protection for your applications and to get access to all the features.
data/lib/commands.rb CHANGED
@@ -261,8 +261,8 @@ class EtnaApp
261
261
  model_attributes_mask: model_attribute_pairs(project_name),
262
262
  record_names: 'all',
263
263
  model_filters: {},
264
- metis_client: metis_client,
265
- magma_client: magma_client,
264
+ metis_client: metis_client(logger: logger),
265
+ magma_client: magma_client(logger: logger),
266
266
  logger: logger,
267
267
  project_name: project_name,
268
268
  model_name: 'project', filesystem: filesystem,
data/lib/etna/client.rb CHANGED
@@ -4,10 +4,17 @@ require 'rack/utils'
4
4
 
5
5
  module Etna
6
6
  class Client
7
- def initialize(host, token, routes_available: true, ignore_ssl: false)
7
+
8
+ def initialize(host, token, routes_available: true, ignore_ssl: false, max_retries: 10, backoff_time: 15, logger: nil)
8
9
  @host = host.sub(%r!/$!, '')
9
10
  @token = token
10
11
  @ignore_ssl = ignore_ssl
12
+ @max_retries = max_retries
13
+ @backoff_time = backoff_time
14
+
15
+ default_logger = ::Etna::Logger.new('/dev/stdout', 0, 1048576)
16
+ default_logger.level = ::Logger::WARN
17
+ @logger = logger || default_logger
11
18
 
12
19
  if routes_available
13
20
  set_routes
@@ -55,7 +62,7 @@ module Etna
55
62
  uri = request_uri(endpoint)
56
63
  multipart = Net::HTTP::Post::Multipart.new uri.request_uri, content
57
64
  multipart.add_field('Authorization', "Etna #{@token}")
58
- request(uri, multipart, &block)
65
+ retrier.retry_request(uri, multipart, &block)
59
66
  end
60
67
 
61
68
  def post(endpoint, params = {}, &block)
@@ -80,9 +87,12 @@ module Etna
80
87
 
81
88
  private
82
89
 
90
+ def retrier
91
+ @retrier ||= Retrier.new(ignore_ssl: @ignore_ssl, max_retries: @max_retries, backoff_time: @backoff_time, logger: @logger)
92
+ end
93
+
83
94
  def set_routes
84
95
  response = options('/')
85
- status_check!(response)
86
96
  @routes = JSON.parse(response.body, symbolize_names: true)
87
97
  end
88
98
 
@@ -120,7 +130,7 @@ module Etna
120
130
  uri = request_uri(endpoint)
121
131
  req = type.new(uri.request_uri, request_headers)
122
132
  req.body = params.to_json
123
- request(uri, req, &block)
133
+ retrier.retry_request(uri, req, &block)
124
134
  end
125
135
 
126
136
  def query_request(type, endpoint, params = {}, &block)
@@ -132,7 +142,7 @@ module Etna
132
142
  uri.query = URI.encode_www_form(params)
133
143
  end
134
144
  req = type.new(uri.request_uri, request_headers)
135
- request(uri, req, &block)
145
+ retrier.retry_request(uri, req, &block)
136
146
  end
137
147
 
138
148
  def request_uri(endpoint)
@@ -140,6 +150,8 @@ module Etna
140
150
  end
141
151
 
142
152
  def request_headers
153
+ refresh_token
154
+
143
155
  {
144
156
  'Content-Type' => 'application/json',
145
157
  'Accept' => 'application/json, text/*',
@@ -149,46 +161,234 @@ module Etna
149
161
  )
150
162
  end
151
163
 
152
- def status_check!(response)
153
- status = response.code.to_i
154
- if status >= 400
155
- msg = response.content_type == 'application/json' ?
156
- json_error(response.body) :
157
- response.body
158
- raise Etna::Error.new(msg, status)
159
- end
164
+ def refresh_token
165
+ @token = TokenRefresher.new(@host, @token, @logger).active_token
160
166
  end
161
167
 
162
- def json_error(body)
163
- msg = JSON.parse(body, symbolize_names: true)
164
- if (msg.has_key?(:errors) && msg[:errors].is_a?(Array))
165
- return JSON.generate(msg[:errors])
166
- elsif msg.has_key?(:error)
167
- return JSON.generate(msg[:error])
168
+ class TokenRefresher
169
+ def initialize(host, token, logger)
170
+ @token = token
171
+ @host = host
172
+ @logger = logger
173
+ end
174
+
175
+ def active_token
176
+ token_will_expire? ?
177
+ refresh_token :
178
+ @token
179
+ end
180
+
181
+ private
182
+
183
+ def token_expired?
184
+ # Has the token already expired?
185
+ token_will_expire?(0)
186
+ end
187
+
188
+ def token_will_expire?(offset=3000)
189
+ return false if @token.nil?
190
+
191
+ # Will the user's token expire in the given amount of time?
192
+ payload = @token.split('.')[1]
193
+ return false if payload.nil?
194
+
195
+ epoch_seconds = JSON.parse(Base64.urlsafe_decode64(payload))["exp"]
196
+
197
+ return false if epoch_seconds.nil?
198
+
199
+ expiration = DateTime.strptime(epoch_seconds.to_s, "%s").to_time
200
+ expiration <= DateTime.now.new_offset.to_time + offset
201
+ end
202
+
203
+ def refresh_token
204
+ @logger.debug("Requesting a refreshed token.")
205
+ uri = refresh_uri
206
+ req = Net::HTTP::Post.new(uri.request_uri, request_headers)
207
+ retrier.retry_request(uri, req).body
208
+ end
209
+
210
+ def refresh_uri
211
+ URI("#{janus_host}#{refresh_endpoint}")
212
+ end
213
+
214
+ def janus_host
215
+ @host.gsub(/(metis|magma|timur|polyphemus|janus|gnomon)/, "janus")
216
+ end
217
+
218
+ def refresh_endpoint
219
+ "/api/tokens/generate"
220
+ end
221
+
222
+ def request_headers
223
+ {
224
+ 'Content-Type' => 'application/json',
225
+ 'Accept' => 'application/json, text/*',
226
+ 'Authorization' => "Etna #{@token}"
227
+ }
228
+ end
229
+
230
+ def retrier
231
+ @retrier ||= Retrier.new(max_retries: 5, backoff_time: 10, logger: @logger)
168
232
  end
169
233
  end
170
234
 
171
- def request(uri, data)
172
- if block_given?
173
- verify_mode = @ignore_ssl ?
174
- OpenSSL::SSL::VERIFY_NONE :
175
- OpenSSL::SSL::VERIFY_PEER
176
- Net::HTTP.start(uri.host, uri.port, use_ssl: true, verify_mode: verify_mode, read_timeout: 300) do |http|
177
- http.request(data) do |response|
235
+ class Retrier
236
+ # Ideally the retry code would be centralized with metis_client ...
237
+ # unsure what would be the best approach to do that, at this moment.
238
+ def initialize(ignore_ssl: false, max_retries: 10, backoff_time: 15, logger:)
239
+ @max_retries = max_retries
240
+ @ignore_ssl = ignore_ssl
241
+ @backoff_time = backoff_time
242
+ @logger = logger
243
+ end
244
+
245
+ def retry_request(uri, req, retries: 0, &block)
246
+ retries += 1
247
+
248
+ begin
249
+ @logger.debug("\rWaiting for #{uri.host} restart"+"."*retries+"\x1b[0K")
250
+
251
+ sleep @backoff_time * retries
252
+ end if retries > 1
253
+
254
+ if retries < @max_retries
255
+ begin
256
+ if block_given?
257
+ request(uri, req) do |block_response|
258
+ if net_exceptions.include?(block_response.class)
259
+ @logger.debug("Received #{block_response.class.name}, retrying")
260
+ retry_request(uri, req, retries: retries, &block)
261
+ elsif block_response.is_a?(OpenSSL::SSL::SSLError)
262
+ @logger.debug("SSL error, retrying")
263
+ retry_request(uri, req, retries: retries, &block)
264
+ else
265
+ status_check!(block_response)
266
+ yield block_response
267
+ end
268
+ end
269
+ else
270
+ response = request(uri, req)
271
+ end
272
+ rescue OpenSSL::SSL::SSLError => e
273
+ if e.message =~ /write client hello/
274
+ @logger.debug("SSL error, retrying")
275
+ return retry_request(uri, req, retries: retries)
276
+ end
277
+ raise e
278
+ rescue *net_exceptions => e
279
+ @logger.debug("Received #{e.class.name}, retrying")
280
+ return retry_request(uri, req, retries: retries)
281
+ end
282
+
283
+ begin
284
+ retry_codes = ['503', '502', '504', '408']
285
+ if retry_codes.include?(response.code)
286
+ @logger.debug("Received response with code #{response.code}, retrying")
287
+ return retry_request(uri, req, retries: retries)
288
+ elsif response.code == '500' && response.body.start_with?("Puma caught")
289
+ @logger.debug("Received 500 Puma error #{response.body.split("\n").first}, retrying")
290
+ return retry_request(uri, req, retries: retries)
291
+ end
292
+
178
293
  status_check!(response)
179
- yield response
294
+ return response
295
+ end unless block_given?
296
+ end
297
+
298
+ raise ::Etna::Error, "Could not contact server, giving up" unless block_given?
299
+ end
300
+
301
+ private
302
+
303
+ def request(uri, data)
304
+ if block_given?
305
+ verify_mode = @ignore_ssl ?
306
+ OpenSSL::SSL::VERIFY_NONE :
307
+ OpenSSL::SSL::VERIFY_PEER
308
+ Net::HTTP.start(uri.host, uri.port, use_ssl: true, verify_mode: verify_mode, read_timeout: 300) do |http|
309
+ http.request(data) do |response|
310
+ api_error_check!(response)
311
+ yield response
312
+ end
313
+ end
314
+ else
315
+ verify_mode = @ignore_ssl ?
316
+ OpenSSL::SSL::VERIFY_NONE :
317
+ OpenSSL::SSL::VERIFY_PEER
318
+ Net::HTTP.start(uri.host, uri.port, use_ssl: true, verify_mode: verify_mode, read_timeout: 300) do |http|
319
+ response = http.request(data)
320
+ api_error_check!(response)
321
+ return response
180
322
  end
181
323
  end
182
- else
183
- verify_mode = @ignore_ssl ?
184
- OpenSSL::SSL::VERIFY_NONE :
185
- OpenSSL::SSL::VERIFY_PEER
186
- Net::HTTP.start(uri.host, uri.port, use_ssl: true, verify_mode: verify_mode, read_timeout: 300) do |http|
187
- response = http.request(data)
188
- status_check!(response)
189
- return response
324
+ end
325
+
326
+ def api_error_check!(response)
327
+ status = response.code.to_i
328
+ raise_status_error(response) if 400 <= status && status < 500
329
+ end
330
+
331
+ def status_check!(response)
332
+ status = response.code.to_i
333
+ raise_status_error(response) if status >= 400
334
+ end
335
+
336
+ def raise_status_error(response)
337
+ msg = response.content_type == 'application/json' ?
338
+ json_error(response.body) :
339
+ response.body
340
+ raise Etna::Error.new(msg, response.code.to_i)
341
+ end
342
+
343
+ def json_error(body)
344
+ msg = JSON.parse(body, symbolize_names: true)
345
+ if (msg.has_key?(:errors) && msg[:errors].is_a?(Array))
346
+ return JSON.generate(msg[:errors])
347
+ elsif msg.has_key?(:error)
348
+ return JSON.generate(msg[:error])
190
349
  end
191
350
  end
351
+
352
+ def net_exceptions
353
+ retry_exceptions = [
354
+ Errno::ECONNREFUSED,
355
+ Errno::ECONNRESET,
356
+ Errno::ENETRESET,
357
+ Errno::EPIPE,
358
+ Errno::ECONNABORTED,
359
+ Errno::EHOSTDOWN,
360
+ Errno::EHOSTUNREACH,
361
+ Errno::EINVAL,
362
+ Errno::ETIMEDOUT,
363
+ Net::ReadTimeout,
364
+ Net::HTTPFatalError,
365
+ Net::HTTPBadResponse,
366
+ Net::HTTPHeaderSyntaxError,
367
+ Net::ProtocolError,
368
+ Net::HTTPRequestTimeOut,
369
+ Net::HTTPGatewayTimeOut,
370
+ Net::HTTPBadRequest,
371
+ Net::HTTPBadGateway,
372
+ Net::HTTPError,
373
+ Net::HTTPInternalServerError,
374
+ Net::HTTPRetriableError,
375
+ Net::HTTPServerError,
376
+ Net::HTTPServiceUnavailable,
377
+ Net::HTTPUnprocessableEntity,
378
+ Net::OpenTimeout,
379
+ IOError,
380
+ EOFError,
381
+ Timeout::Error
382
+ ]
383
+
384
+ begin
385
+ retry_exceptions << Net::HTTPRequestTimeout
386
+ retry_exceptions << Net::HTTPGatewayTimeout
387
+ retry_exceptions << Net::WriteTimeout
388
+ end if RUBY_VERSION > "2.5.8"
389
+
390
+ retry_exceptions
391
+ end
192
392
  end
193
393
  end
194
394
  end
@@ -6,7 +6,7 @@ module Etna
6
6
  module Clients
7
7
  class BaseClient
8
8
  attr_reader :host, :token, :ignore_ssl
9
- def initialize(host:, token:, ignore_ssl: false)
9
+ def initialize(host:, token:, ignore_ssl: false, logger: nil)
10
10
  raise "#{self.class.name} client configuration is missing host." unless host
11
11
 
12
12
  @token = token
@@ -16,7 +16,8 @@ module Etna
16
16
  host,
17
17
  token,
18
18
  routes_available: false,
19
- ignore_ssl: ignore_ssl)
19
+ ignore_ssl: ignore_ssl,
20
+ logger: logger)
20
21
  @host = host
21
22
  @ignore_ssl = ignore_ssl
22
23
  end
@@ -94,7 +94,6 @@ module Etna
94
94
  collections = []
95
95
  links = []
96
96
  attributes = []
97
-
98
97
  model = response.models.model(model_name)
99
98
 
100
99
  template.attributes.attribute_keys.each do |attr_name|
@@ -10,10 +10,10 @@ module Etna
10
10
  module Clients
11
11
  class Metis < Etna::Clients::BaseClient
12
12
 
13
- def initialize(host:, token:, ignore_ssl: false)
13
+ def initialize(host:, token:, ignore_ssl: false, logger: nil)
14
14
  raise 'Metis client configuration is missing host.' unless host
15
15
  raise 'Metis client configuration is missing token.' unless token
16
- @etna_client = ::Etna::Client.new(host, token, ignore_ssl: ignore_ssl)
16
+ @etna_client = ::Etna::Client.new(host, token, ignore_ssl: ignore_ssl, logger: logger)
17
17
 
18
18
  @token = token
19
19
  end
@@ -31,9 +31,17 @@ module Etna::Spec
31
31
  def below_admin_roles
32
32
  [:editor, :viewer, :guest]
33
33
  end
34
-
34
+
35
35
  def below_editor_roles
36
36
  [:viewer, :guest]
37
37
  end
38
+
39
+ def stub_janus_refresh
40
+ stub_request(:post, /\/api\/tokens\/generate/)
41
+ .to_return({
42
+ status: 200,
43
+ body: ""
44
+ })
45
+ end
38
46
  end
39
47
  end
data/lib/helpers.rb CHANGED
@@ -43,17 +43,19 @@ module WithEtnaClients
43
43
  env_token
44
44
  end
45
45
 
46
- def magma_client
46
+ def magma_client(logger: nil)
47
47
  @magma_client ||= Etna::Clients::Magma.new(
48
48
  token: token,
49
49
  ignore_ssl: EtnaApp.instance.config(:ignore_ssl),
50
+ logger: logger,
50
51
  **EtnaApp.instance.config(:magma, environment) || {})
51
52
  end
52
53
 
53
- def metis_client
54
+ def metis_client(logger: nil)
54
55
  @metis_client ||= Etna::Clients::Metis.new(
55
56
  token: token,
56
57
  ignore_ssl: EtnaApp.instance.config(:ignore_ssl),
58
+ logger: logger,
57
59
  **EtnaApp.instance.config(:metis, environment) || {})
58
60
  end
59
61
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: etna
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.47
4
+ version: 0.1.48
5
5
  platform: ruby
6
6
  authors:
7
7
  - Saurabh Asthana
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-01-25 00:00:00.000000000 Z
11
+ date: 2023-02-03 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rack