etna 0.1.27 → 0.1.32

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.
@@ -18,6 +18,56 @@ class DirectedGraph
18
18
  parents[parent] = parent_parents
19
19
  end
20
20
 
21
+ def serialized_path_from(root)
22
+ seen = Set.new
23
+ [].tap do |result|
24
+ result << root
25
+ seen.add(root)
26
+ path_q = paths_from(root)
27
+
28
+ until path_q.empty?
29
+ next_path = path_q.shift
30
+ next if next_path.nil?
31
+
32
+ until next_path.empty?
33
+ next_n = next_path.shift
34
+ next if next_n.nil?
35
+ next if seen.include?(next_n)
36
+
37
+ if @parents[next_n].keys.any? { |p| !seen.include?(p) }
38
+ next_path.unshift(next_n)
39
+ path_q.push(next_path)
40
+ break
41
+ else
42
+ result << next_n
43
+ seen.add(next_n)
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
49
+
50
+ def paths_from(root)
51
+ [].tap do |result|
52
+ parents_of_map = descendants(root)
53
+ seen = Set.new
54
+
55
+ parents_of_map.to_a.sort_by { |k, parents| [-parents.length, k.inspect] }.each do |k, parents|
56
+ unless seen.include?(k)
57
+ if @children[k].keys.empty?
58
+ result << parents + [k]
59
+ else
60
+ @children[k].keys.dup.sort.each do |c|
61
+ result << parents + [k, c]
62
+ end
63
+ end
64
+ end
65
+
66
+ parents.each { |p| seen.add(p) }
67
+ end
68
+ end
69
+ end
70
+
21
71
  def descendants(parent)
22
72
  seen = Set.new
23
73
 
@@ -1,36 +1,52 @@
1
1
  require 'yaml'
2
2
  require 'fileutils'
3
3
  require 'open3'
4
+ require 'securerandom'
5
+ require 'concurrent-ruby'
4
6
 
5
7
  module Etna
6
8
  # A class that encapsulates opening / reading file system entries that abstracts normal file access in order
7
9
  # to make stubbing, substituting, and testing easier.
8
10
  class Filesystem
9
11
  def with_writeable(dest, opts = 'w', size_hint: nil, &block)
12
+ raise "with_writeable not supported by #{self.class.name}" unless self.class == Filesystem
10
13
  ::File.open(dest, opts, &block)
11
14
  end
12
15
 
16
+ def ls(dir)
17
+ raise "ls not supported by #{self.class.name}" unless self.class == Filesystem
18
+ ::Dir.entries(dir).select { |p| !p.start_with?('.') }.map do |path|
19
+ ::File.file?(::File.join(dir, path)) ? [:file, path] : [:dir, path]
20
+ end
21
+ end
22
+
13
23
  def with_readable(src, opts = 'r', &block)
24
+ raise "with_readable not supported by #{self.class.name}" unless self.class == Filesystem
14
25
  ::File.open(src, opts, &block)
15
26
  end
16
27
 
17
28
  def mkdir_p(dir)
29
+ raise "mkdir_p not supported by #{self.class.name}" unless self.class == Filesystem
18
30
  ::FileUtils.mkdir_p(dir)
19
31
  end
20
32
 
21
33
  def rm_rf(dir)
34
+ raise "rm_rf not supported by #{self.class.name}" unless self.class == Filesystem
22
35
  ::FileUtils.rm_rf(dir)
23
36
  end
24
37
 
25
38
  def tmpdir
39
+ raise "tmpdir not supported by #{self.class.name}" unless self.class == Filesystem
26
40
  ::Dir.mktmpdir
27
41
  end
28
42
 
29
43
  def exist?(src)
44
+ raise "exist? not supported by #{self.class.name}" unless self.class == Filesystem
30
45
  ::File.exist?(src)
31
46
  end
32
47
 
33
48
  def mv(src, dest)
49
+ raise "mv not supported by #{self.class.name}" unless self.class == Filesystem
34
50
  ::FileUtils.mv(src, dest)
35
51
  end
36
52
 
@@ -179,7 +195,7 @@ module Etna
179
195
  cmd << remote_path
180
196
  cmd << local_path
181
197
 
182
- cmd << { out: wd }
198
+ cmd << {out: wd}
183
199
  elsif opts.include?('w')
184
200
  cmd << '--mode=send'
185
201
  cmd << "--host=#{@host}"
@@ -187,7 +203,7 @@ module Etna
187
203
  cmd << local_path
188
204
  cmd << remote_path
189
205
 
190
- cmd << { in: rd }
206
+ cmd << {in: rd}
191
207
  end
192
208
 
193
209
  cmd
@@ -196,23 +212,10 @@ module Etna
196
212
 
197
213
  # Genentech's aspera deployment doesn't support modern commands, unfortunately...
198
214
  class GeneAsperaCliFilesystem < AsperaCliFilesystem
199
- def tmpdir
200
- raise "tmpdir is not supported"
201
- end
202
-
203
- def rm_rf
204
- raise "rm_rf is not supported"
205
- end
206
-
207
215
  def mkdir_p(dest)
208
216
  # Pass through -- this file system creates containing directories by default, womp womp.
209
217
  end
210
218
 
211
- def mv
212
- raise "mv is not supported"
213
- end
214
-
215
-
216
219
  def mkcommand(rd, wd, file, opts, size_hint: nil)
217
220
  if opts.include?('w')
218
221
  super.map do |e|
@@ -230,6 +233,8 @@ module Etna
230
233
  end
231
234
 
232
235
  def with_writeable(dest, opts = 'w', size_hint: nil, &block)
236
+ raise "#{self.class.name} requires size_hint in with_writeable" if size_hint.nil?
237
+
233
238
  super do |io|
234
239
  io.write("File: #{::File.basename(dest)}\n")
235
240
  io.write("Size: #{size_hint}\n")
@@ -239,6 +244,131 @@ module Etna
239
244
  end
240
245
  end
241
246
 
247
+ class Metis < Filesystem
248
+ def initialize(metis_client:, project_name:, bucket_name:, root: '/', uuid: SecureRandom.uuid)
249
+ @metis_client = metis_client
250
+ @project_name = project_name
251
+ @bucket_name = bucket_name
252
+ @root = root
253
+ @metis_uid = uuid
254
+ end
255
+
256
+ def metis_path_of(path)
257
+ joined = ::File.join(@root, path)
258
+ joined[0] == "/" ? joined.slice(1..-1) : joined
259
+ end
260
+
261
+ def create_upload_workflow
262
+ Etna::Clients::Metis::MetisUploadWorkflow.new(metis_client: @metis_client, metis_uid: @metis_uid, project_name: @project_name, bucket_name: @bucket_name, max_attempts: 3)
263
+ end
264
+
265
+ def with_hot_pipe(opts, receiver, *args, &block)
266
+ rp, wp = IO.pipe
267
+ begin
268
+ executor = Concurrent::SingleThreadExecutor.new(fallback_policy: :abort)
269
+ begin
270
+ if opts.include?('w')
271
+ future = Concurrent::Promises.future_on(executor) do
272
+ self.send(receiver, rp, *args)
273
+ rescue => e
274
+ Etna::Application.instance.logger.log_error(e)
275
+ raise e
276
+ ensure
277
+ rp.close
278
+ end
279
+
280
+ yield wp
281
+ else
282
+ future = Concurrent::Promises.future_on(executor) do
283
+ self.send(receiver, wp, *args)
284
+ rescue => e
285
+ Etna::Application.instance.logger.log_error(e)
286
+ raise e
287
+ ensure
288
+ wp.close
289
+ end
290
+
291
+ yield rp
292
+ end
293
+
294
+ future.wait!
295
+ ensure
296
+ executor.shutdown
297
+ executor.kill unless executor.wait_for_termination(5)
298
+ end
299
+ ensure
300
+ rp.close
301
+ wp.close
302
+ end
303
+ end
304
+
305
+ def do_streaming_upload(rp, dest, size_hint)
306
+ streaming_upload = Etna::Clients::Metis::MetisUploadWorkflow::StreamingIOUpload.new(readable_io: rp, size_hint: size_hint)
307
+ create_upload_workflow.do_upload(
308
+ streaming_upload,
309
+ metis_path_of(dest)
310
+ )
311
+ end
312
+
313
+ def with_writeable(dest, opts = 'w', size_hint: nil, &block)
314
+ self.with_hot_pipe(opts, :do_streaming_upload, dest, size_hint) do |wp|
315
+ yield wp
316
+ end
317
+ end
318
+
319
+ def create_download_workflow
320
+ Etna::Clients::Metis::MetisDownloadWorkflow.new(metis_client: @metis_client, project_name: @project_name, bucket_name: @bucket_name, max_attempts: 3)
321
+ end
322
+
323
+ def do_streaming_download(wp, metis_file)
324
+ create_download_workflow.do_download(wp, metis_file)
325
+ end
326
+
327
+ def with_readable(src, opts = 'r', &block)
328
+ metis_file = list_metis_directory(::File.dirname(src)).files.all.find { |f| f.file_name == ::File.basename(src) }
329
+ raise "Metis file at #{@project_name}/#{@bucket_name}/#{@root}/#{src} not found. No such file" if metis_file.nil?
330
+
331
+ self.with_hot_pipe(opts, :do_streaming_download, metis_file) do |rp|
332
+ yield rp
333
+ end
334
+ end
335
+
336
+ def list_metis_directory(path)
337
+ @metis_client.list_folder(Etna::Clients::Metis::ListFolderRequest.new(project_name: @project_name, bucket_name: @bucket_name, folder_path: metis_path_of(path)))
338
+ end
339
+
340
+ def mkdir_p(dir)
341
+ create_folder_request = Etna::Clients::Metis::CreateFolderRequest.new(
342
+ project_name: @project_name,
343
+ bucket_name: @bucket_name,
344
+ folder_path: metis_path_of(dir),
345
+ )
346
+ @metis_client.create_folder(create_folder_request)
347
+ end
348
+
349
+ def ls(dir)
350
+ response = list_metis_directory(::File.dirname(dir))
351
+ response.files.map { |f| [:file, f.file_name] } + response.folders.map { |f| [:folder, f.folder_name] }
352
+ end
353
+
354
+ def exist?(src)
355
+ begin
356
+ response = list_metis_directory(::File.dirname(src))
357
+ rescue Etna::Error => e
358
+ if e.status == 404
359
+ return false
360
+ elsif e.message =~ /Invalid folder/
361
+ return false
362
+ end
363
+
364
+ raise e
365
+ end
366
+
367
+ response.files.all.any? { |f| f.file_name == ::File.basename(src) } ||
368
+ response.folders.all.any? { |f| f.folder_name == ::File.basename(src) }
369
+ end
370
+ end
371
+
242
372
  class Mock < Filesystem
243
373
  def initialize(&new_io)
244
374
  @files = {}
@@ -29,10 +29,12 @@ module Etna
29
29
  write %Q(elif [[ -z "$(echo $all_flag_completion_names | xargs)" ]]; then)
30
30
  write "return"
31
31
  write %Q(elif [[ "$all_flag_completion_names" =~ $1\\ ]]; then)
32
+ write %Q(if ! [[ "$multi_flags" =~ $1\\ ]]; then)
32
33
  write %Q(all_flag_completion_names="${all_flag_completion_names//$1\\ /}")
34
+ write 'fi'
33
35
  write 'a=$1'
34
36
  write 'shift'
35
- write %Q(if [[ "$string_flag_completion_names" =~ $a\\ ]]; then)
37
+ write %Q(if [[ "$arg_flag_completion_names" =~ $a\\ ]]; then)
36
38
  write 'if [[ "$#" == "1" ]]; then'
37
39
  write %Q(a="${a//--/}")
38
40
  write %Q(a="${a//-/_}")
@@ -58,12 +60,14 @@ module Etna
58
60
 
59
61
  def enable_flags(flags_container)
60
62
  boolean_flags = flags_container.boolean_flags
61
- string_flags = flags_container.string_flags
62
- flags = boolean_flags + string_flags
63
+ multi_flags = flags_container.multi_flags
64
+ arg_flags = flags_container.string_flags + multi_flags
65
+ flags = boolean_flags + arg_flags
63
66
  write %Q(all_flag_completion_names="$all_flag_completion_names #{flags.join(' ')} ")
64
- write %Q(string_flag_completion_names="$string_flag_completion_names #{string_flags.join(' ')} ")
67
+ write %Q(arg_flag_completion_names="$arg_flag_completion_names #{arg_flags.join(' ')} ")
68
+ write %Q(multi_flags="$multi_flags #{multi_flags.join(' ')} ")
65
69
 
66
- string_flags.each do |flag|
70
+ arg_flags.each do |flag|
67
71
  write %Q(declare _completions_for_#{flag_as_parameter(flag)}="#{completions_for(flag_as_parameter(flag)).join(' ')}")
68
72
  end
69
73
  end
@@ -107,7 +111,8 @@ function _#{name}_completions() {
107
111
 
108
112
  function _#{name}_inner_completions() {
109
113
  local all_flag_completion_names=''
110
- local string_flag_completion_names=''
114
+ local arg_flag_completion_names=''
115
+ local multi_flags=''
111
116
  local all_completion_names=''
112
117
  local i=''
113
118
  local a=''
data/lib/etna/hmac.rb CHANGED
@@ -20,14 +20,14 @@ module Etna
20
20
  end
21
21
 
22
22
  # this returns arguments for URI::HTTP.build
23
- def url_params
23
+ def url_params(with_headers=true)
24
24
  params = {
25
25
  signature: signature,
26
26
  expiration: @expiration,
27
27
  nonce: @nonce,
28
28
  id: @id.to_s,
29
29
  headers: @headers.keys.join(','),
30
- }.merge(@headers).map do |name, value|
30
+ }.merge(with_headers ? @headers : {}).map do |name, value|
31
31
  [
32
32
  "X-Etna-#{ name.to_s.split(/_/).map(&:capitalize).join('-') }",
33
33
  value
data/lib/etna/route.rb CHANGED
@@ -9,6 +9,7 @@ module Etna
9
9
  @name = route_name(options)
10
10
  @route = route.gsub(/\A(?=[^\/])/, '/')
11
11
  @block = block
12
+ @match_ext = options[:match_ext]
12
13
  end
13
14
 
14
15
  def to_hash
@@ -72,9 +73,7 @@ module Etna
72
73
  user = request.env['etna.user']
73
74
 
74
75
  params = request.env['rack.request.params'].map do |key,value|
75
- value = value.to_s
76
- value = value[0..500] + "..." + value[-100..-1] if value.length > 600
77
- [ key, value ]
76
+ [ key, redact(key, value) ]
78
77
  end.to_h
79
78
 
80
79
  logger.warn("User #{user ? user.email : :unknown} calling #{controller}##{action} with params #{params}")
@@ -94,6 +93,39 @@ module Etna
94
93
 
95
94
  private
96
95
 
96
+ def application
97
+ @application ||= Etna::Application.instance
98
+ end
99
+
100
+ def compact(value)
101
+ value = value.to_s
102
+ value = value[0..500] + "..." + value[-100..-1] if value.length > 600
103
+ value
104
+ end
105
+
106
+ def redact_keys
107
+ @redact_keys ||= application.config(:log_redact_keys).split(",").map do |key|
108
+ key.to_sym
109
+ end
110
+ end
111
+
112
+ def redact(key, value)
113
+ # From configuration, redact any values for the supplied key values, so they
114
+ # don't appear in the logs.
115
+ return compact(value) unless application.config(:log_redact_keys)
116
+
117
+ if value.is_a?(Hash)
118
+ redacted_value = value.map do |value_key, value_value|
119
+ [ value_key, redact(value_key, value_value) ]
120
+ end.to_h
121
+ return redacted_value
122
+ elsif redact_keys.include?(key)
123
+ return "*"
124
+ end
125
+
126
+ return compact(value)
127
+ end
128
+
97
129
  def authorized?(request)
98
130
  # If there is no @auth requirement, they are ok - this doesn't preclude
99
131
  # them being rejected in the controller response
@@ -145,13 +177,21 @@ module Etna
145
177
  )
146
178
  end
147
179
 
180
+ def separator_free_match
181
+ if @match_ext
182
+ '(?<\1>[^\/\?]+)'
183
+ else
184
+ '(?<\1>[^\.\/\?]+)'
185
+ end
186
+ end
187
+
148
188
  def route_regexp
149
189
  @route_regexp ||=
150
190
  Regexp.new(
151
191
  '\A' +
152
192
  @route.
153
193
  # any :params match separator-free strings
154
- gsub(NAMED_PARAM, '(?<\1>[^\.\/\?]+)').
194
+ gsub(NAMED_PARAM, separator_free_match).
155
195
  # any *params match arbitrary strings
156
196
  gsub(GLOB_PARAM, '(?<\1>.+)').
157
197
  # ignore any trailing slashes in the route
@@ -2,22 +2,22 @@ module Etna::Spec
2
2
  module Auth
3
3
  AUTH_USERS = {
4
4
  superuser: {
5
- email: 'zeus@olympus.org', first: 'Zeus', perm: 'A:administration'
5
+ email: 'zeus@olympus.org', name: 'Zeus', perm: 'A:administration'
6
6
  },
7
7
  admin: {
8
- email: 'hera@olympus.org', first: 'Hera', perm: 'a:labors'
8
+ email: 'hera@olympus.org', name: 'Hera', perm: 'a:labors'
9
9
  },
10
10
  editor: {
11
- email: 'eurystheus@twelve-labors.org', first: 'Eurystheus', perm: 'E:labors'
11
+ email: 'eurystheus@twelve-labors.org', name: 'Eurystheus', perm: 'E:labors'
12
12
  },
13
13
  restricted_editor: {
14
- email: 'copreus@twelve-labors.org', first: 'Copreus', perm: 'e:labors'
14
+ email: 'copreus@twelve-labors.org', name: 'Copreus', perm: 'e:labors'
15
15
  },
16
16
  viewer: {
17
- email: 'hercules@twelve-labors.org', first: 'Hercules', perm: 'v:labors'
17
+ email: 'hercules@twelve-labors.org', name: 'Hercules', perm: 'v:labors'
18
18
  },
19
19
  non_user: {
20
- email: 'nessus@centaurs.org', first: 'Nessus', perm: ''
20
+ email: 'nessus@centaurs.org', name: 'Nessus', perm: ''
21
21
  }
22
22
  }
23
23