etna 0.1.27 → 0.1.32

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