etna 0.1.43 → 0.1.46

Sign up to get free protection for your applications and to get access to all the features.
Files changed (37) hide show
  1. checksums.yaml +4 -4
  2. data/bin/etna +12 -1
  3. data/etna.completion +36788 -1
  4. data/lib/commands.rb +118 -0
  5. data/lib/etna/application.rb +13 -16
  6. data/lib/etna/auth.rb +37 -14
  7. data/lib/etna/clients/janus/client.rb +17 -18
  8. data/lib/etna/clients/janus/models.rb +52 -0
  9. data/lib/etna/clients/magma/client.rb +41 -0
  10. data/lib/etna/clients/magma/formatting/models_csv.rb +14 -4
  11. data/lib/etna/clients/magma/models.rb +19 -8
  12. data/lib/etna/clients/magma/workflows/create_project_workflow.rb +1 -1
  13. data/lib/etna/clients/magma/workflows/json_validators.rb +5 -5
  14. data/lib/etna/clients/magma/workflows/model_synchronization_workflow.rb +9 -9
  15. data/lib/etna/clients/metis/client.rb +7 -2
  16. data/lib/etna/clients/metis/models.rb +6 -3
  17. data/lib/etna/clients/metis/workflows/metis_download_workflow.rb +1 -1
  18. data/lib/etna/clients/metis/workflows/sync_metis_data_workflow.rb +23 -0
  19. data/lib/etna/clients/metis/workflows/walk_metis_diff_workflow.rb +95 -0
  20. data/lib/etna/clients/metis/workflows/walk_metis_workflow.rb +31 -0
  21. data/lib/etna/clients/metis/workflows.rb +2 -0
  22. data/lib/etna/controller.rb +48 -1
  23. data/lib/etna/cwl.rb +8 -0
  24. data/lib/etna/environment_variables.rb +50 -0
  25. data/lib/etna/filesystem.rb +2 -0
  26. data/lib/etna/injection.rb +57 -0
  27. data/lib/etna/janus_utils.rb +55 -0
  28. data/lib/etna/permissions.rb +104 -0
  29. data/lib/etna/redirect.rb +36 -0
  30. data/lib/etna/route.rb +54 -5
  31. data/lib/etna/server.rb +13 -1
  32. data/lib/etna/spec/auth.rb +11 -0
  33. data/lib/etna/spec/vcr.rb +28 -3
  34. data/lib/etna/test_auth.rb +25 -1
  35. data/lib/etna/user.rb +4 -26
  36. data/lib/etna.rb +4 -0
  37. metadata +13 -6
@@ -24,8 +24,13 @@ module Etna
24
24
  end
25
25
 
26
26
  def list_folder(list_folder_request = ListFolderRequest.new)
27
- FoldersAndFilesResponse.new(
28
- @etna_client.folder_list(list_folder_request.to_h))
27
+ if list_folder_request.folder_path != ''
28
+ FoldersAndFilesResponse.new(
29
+ @etna_client.folder_list(list_folder_request.to_h))
30
+ else
31
+ FoldersAndFilesResponse.new(
32
+ @etna_client.bucket_list(list_folder_request.to_h))
33
+ end
29
34
  end
30
35
 
31
36
  def list_folder_by_id(list_folder_by_id_request = ListFolderByIdRequest.new)
@@ -50,7 +50,10 @@ module Etna
50
50
  end
51
51
  end
52
52
 
53
- class ListFolderRequest < Struct.new(:project_name, :bucket_name, :folder_path, keyword_init: true)
53
+ class FolderRequest < Struct.new(:project_name, :bucket_name, :folder_path, keyword_init: true)
54
+ end
55
+
56
+ class ListFolderRequest < FolderRequest
54
57
  include JsonSerializableStruct
55
58
 
56
59
  def initialize(**params)
@@ -110,7 +113,7 @@ module Etna
110
113
  end
111
114
  end
112
115
 
113
- class CreateFolderRequest < Struct.new(:project_name, :bucket_name, :folder_path, keyword_init: true)
116
+ class CreateFolderRequest < FolderRequest
114
117
  include JsonSerializableStruct
115
118
 
116
119
  def initialize(**params)
@@ -125,7 +128,7 @@ module Etna
125
128
  end
126
129
  end
127
130
 
128
- class DeleteFolderRequest < Struct.new(:project_name, :bucket_name, :folder_path, keyword_init: true)
131
+ class DeleteFolderRequest < FolderRequest
129
132
  include JsonSerializableStruct
130
133
 
131
134
  def initialize(**params)
@@ -18,7 +18,7 @@ module Etna
18
18
  completed = 0.0
19
19
  start = Time.now
20
20
 
21
- unless dest_file_or_io.is_a?(IO)
21
+ unless dest_file_or_io.is_a?(IO) || dest_file_or_io.is_a?(StringIO)
22
22
  ::File.open(dest_file_or_io, 'w') do |io|
23
23
  return do_download(dest_file_or_io, metis_file, &block)
24
24
  end
@@ -6,6 +6,8 @@ module Etna
6
6
  module Clients
7
7
  class Metis
8
8
  class SyncMetisDataWorkflow < Struct.new(:metis_client, :filesystem, :project_name, :bucket_name, :logger, keyword_init: true)
9
+ DOWNLOAD_REGEX = /^https:\/\/[^\/]*\/(?<project_name>.*)\/download\/(?<bucket_name>.*)\/(?<file_path>[^\?]*).*$/
10
+
9
11
  def copy_directory(src, dest, root = dest)
10
12
  response = metis_client.list_folder(ListFolderRequest.new(project_name: project_name, bucket_name: bucket_name, folder_path: src))
11
13
 
@@ -20,6 +22,27 @@ module Etna
20
22
  end
21
23
 
22
24
  def copy_file(dest:, url:, stub: false)
25
+ # This does not work due to the magma bucket's restrictions, but if it did work, it'd be super sweet.
26
+ # url_match = DOWNLOAD_REGEX.match(url)
27
+ #
28
+ # if filesystem.instance_of?(Etna::Filesystem::Metis) && !url_match.nil?
29
+ # bucket_name = url_match[:bucket_name]
30
+ # project_name = url_match[:project_name]
31
+ # file_path = url_match[:file_path]
32
+ #
33
+ # metis_client.copy_files(
34
+ # Etna::Clients::Metis::CopyFilesRequest.new(
35
+ # project_name: project_name,
36
+ # revisions: [
37
+ # Etna::Clients::Metis::CopyRevision.new(
38
+ # source: "metis://#{project_name}/#{bucket_name}/#{file_path}",
39
+ # dest: "metis://#{filesystem.project_name}/#{filesystem.bucket_name}#{dest}",
40
+ # )
41
+ # ]
42
+ # )
43
+ # )
44
+ # end
45
+
23
46
  metadata = metis_client.file_metadata(url)
24
47
  size = metadata[:size]
25
48
 
@@ -0,0 +1,95 @@
1
+ module Etna
2
+ module Clients
3
+ class Metis
4
+ class WalkMetisDiffWorkflow < Struct.new(:left_walker, :right_walker, keyword_init: true)
5
+ # Iterates entries of the form [kind, left | nil, right | nil]
6
+ # where kind is one of
7
+ # :left_unique | :right_unique | :left_is_folder | :right_is_folder
8
+ # :unknown | :equal | :right_older | :left_older
9
+ # and left / right is one of
10
+ # nil | Etna::Clients::Metis::File | Etna::Clients::Metis::Folder
11
+ def each(&block)
12
+ left_enum = self.left_walker.to_enum
13
+ right_enum = self.right_walker.to_enum
14
+
15
+ l, l_path = next_or_nil(left_enum)
16
+ r, r_path = next_or_nil(right_enum)
17
+
18
+ while l && r
19
+ if l_path == r_path
20
+ yield [compare_file_or_folders(l, r), l, r]
21
+
22
+ l, l_path = next_or_nil(left_enum)
23
+ r, r_path = next_or_nil(right_enum)
24
+ elsif l_path < r_path
25
+ yield [:left_unique, l, nil]
26
+ l, l_path = next_or_nil(left_enum)
27
+ else
28
+ yield [:right_unique, nil, r]
29
+ r, r_path = next_or_nil(right_enum)
30
+ end
31
+ end
32
+
33
+ while l
34
+ yield [:left_unique, l, nil]
35
+ l, l_path = next_or_nil(left_enum)
36
+ end
37
+
38
+ while r
39
+ yield [:right_unique, nil, r]
40
+ r, r_path = next_or_nil(right_enum)
41
+ end
42
+ end
43
+
44
+ def next_or_nil(enum)
45
+ enum.next
46
+ rescue StopIteration
47
+ [nil, nil]
48
+ end
49
+
50
+ def compare_file_or_folders(l, r)
51
+ if l.is_a?(Etna::Clients::Metis::Folder)
52
+ if r.is_a?(Etna::Clients::Metis::Folder)
53
+ return compare_file_or_folder_age(l, r)
54
+ end
55
+
56
+ return :left_is_folder
57
+ end
58
+
59
+ if r.is_a?(Etna::Clients::Metis::Folder)
60
+ return :right_is_folder
61
+ end
62
+
63
+
64
+ if l.file_hash.nil? || r.file_hash.nil?
65
+ return :unknown
66
+ end
67
+
68
+ if l.file_hash == r.file_hash
69
+ return :equal
70
+ end
71
+
72
+ return compare_file_or_folder_age(l, r)
73
+ end
74
+
75
+ def compare_file_or_folder_age(l, r)
76
+ if l.updated_at.nil?
77
+ return :unknown
78
+ end
79
+
80
+ if r.updated_at.nil?
81
+ return :unknown
82
+ end
83
+
84
+ if l.updated_at < r.updated_at
85
+ return :left_older
86
+ elsif l.updated_at > r.updated_at
87
+ return :right_older
88
+ else
89
+ return :equal
90
+ end
91
+ end
92
+ end
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,31 @@
1
+ module Etna
2
+ module Clients
3
+ class Metis
4
+ class WalkMetisWorkflow < Struct.new(:metis_client, :project_name,
5
+ :bucket_name, :logger, :root_dir, keyword_init: true)
6
+ def each(&block)
7
+ q = [self.root_dir]
8
+
9
+ while (n = q.pop)
10
+ req = Etna::Clients::Metis::ListFolderRequest.new(
11
+ project_name: project_name,
12
+ bucket_name: bucket_name,
13
+ folder_path: n
14
+ )
15
+ next unless metis_client.folder_exists?(req)
16
+ resp = metis_client.list_folder(req)
17
+
18
+ resp.files.all.sort_by { |f| f.file_path[self.root_dir.length..-1] }.each do |file|
19
+ yield [file, file.file_path[self.root_dir.length..-1]]
20
+ end
21
+
22
+ resp.folders.all.sort_by { |f| f.folder_path[self.root_dir.length..-1] }.each do |f|
23
+ yield [f, f.folder_path[self.root_dir.length..-1]]
24
+ q << f.folder_path
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -2,3 +2,5 @@ require_relative "./workflows/metis_download_workflow"
2
2
  require_relative "./workflows/metis_upload_workflow"
3
3
  require_relative "./workflows/sync_metis_data_workflow"
4
4
  require_relative "./workflows/ingest_metis_data_workflow"
5
+ require_relative "./workflows/walk_metis_workflow"
6
+ require_relative "./workflows/walk_metis_diff_workflow"
@@ -1,4 +1,5 @@
1
1
  require 'erb'
2
+ require 'net/smtp'
2
3
 
3
4
  module Etna
4
5
  class Controller
@@ -44,10 +45,56 @@ module Etna
44
45
  rescue Exception => e
45
46
  error = e
46
47
  ensure
47
- log_request
48
+ log_request if !@request.env['etna.dont_log'] || error
48
49
  return handle_error(error) if error
49
50
  end
50
51
 
52
+ def try_stream(content_type, &block)
53
+ if @request.env['rack.hijack?']
54
+ @request.env['rack.hijack'].call
55
+ stream = @request.env['rack.hijack_io']
56
+
57
+ headers = [
58
+ "HTTP/1.1 200 OK",
59
+ "Content-Type: #{content_type}"
60
+ ]
61
+ stream.write(headers.map { |header| header + "\r\n" }.join)
62
+ stream.write("\r\n")
63
+ stream.flush
64
+
65
+ Thread.new do
66
+ block.call(stream)
67
+ ensure
68
+ stream.close
69
+ end
70
+
71
+ # IO is now streaming and will be processed by above thread.
72
+ @response.close
73
+ else
74
+ @response['Content-Type'] = content_type
75
+ block.call(@response)
76
+ @response.finish
77
+ end
78
+ end
79
+
80
+ def send_email(to_name, to_email, subject, content)
81
+ message = <<MESSAGE_END
82
+ From: Data Library <noreply@janus>
83
+ To: #{to_name} <#{to_email}>
84
+ Subject: #{subject}
85
+
86
+ #{content}
87
+ MESSAGE_END
88
+
89
+ unless @server.send(:application).test?
90
+ Net::SMTP.start('smtp.ucsf.edu') do |smtp|
91
+ smtp.send_message message, 'noreply@janus', to_email
92
+ end
93
+ end
94
+ rescue => e
95
+ @logger.log_error(e)
96
+ end
97
+
51
98
  def require_params(*params)
52
99
  missing_params = params.reject{|p| @params.key?(p) }
53
100
  raise Etna::BadRequest, "Missing param #{missing_params.join(', ')}" unless missing_params.empty?
data/lib/etna/cwl.rb CHANGED
@@ -559,6 +559,14 @@ module Etna
559
559
  def id
560
560
  @attributes['id']
561
561
  end
562
+
563
+ def type
564
+ @attributes['type']
565
+ end
566
+
567
+ def format
568
+ @attributes['format']
569
+ end
562
570
  end
563
571
 
564
572
  class WorkflowInputParameter < Cwl
@@ -0,0 +1,50 @@
1
+ module Etna
2
+ module EnvironmentVariables
3
+ # a <- b
4
+ def self.deep_merge(a, b)
5
+ if a.is_a?(Hash)
6
+ if b.is_a?(Hash)
7
+ b.keys.each do |b_key|
8
+ a[b_key] = deep_merge(a[b_key], b[b_key])
9
+ end
10
+
11
+ return a
12
+ end
13
+ end
14
+
15
+ a.nil? ? b : a
16
+ end
17
+
18
+ def self.load_from_env(prefix, root: {}, env: ENV, downcase: true, sep: '__', &path_to_value_mapper)
19
+ env.keys.each do |key|
20
+ next unless key.start_with?(prefix + sep)
21
+
22
+ path = key.split(sep, -1)
23
+ path.shift
24
+ if downcase
25
+ path.each(&:downcase!)
26
+ end
27
+
28
+ result = path_to_value_mapper.call(path, env[key])
29
+ next unless result
30
+
31
+ path, value = result
32
+
33
+ if path.empty?
34
+ root = EnvironmentVariables.deep_merge(root, value)
35
+ next
36
+ end
37
+
38
+ target = root
39
+ while path.length > 1
40
+ n = path.shift
41
+ target = (target[n] ||= {})
42
+ end
43
+
44
+ target[path.last] = EnvironmentVariables.deep_merge(target[path.last], value)
45
+ end
46
+
47
+ root
48
+ end
49
+ end
50
+ end
@@ -251,6 +251,8 @@ module Etna
251
251
  end
252
252
 
253
253
  class Metis < Filesystem
254
+ attr_reader :project_name, :bucket_name
255
+
254
256
  def initialize(metis_client:, project_name:, bucket_name:, root: '/', uuid: SecureRandom.uuid)
255
257
  @metis_client = metis_client
256
258
  @project_name = project_name
@@ -0,0 +1,57 @@
1
+ module Etna
2
+ class Injection
3
+ # Extend into class
4
+ module FromHash
5
+ def from_hash(hash, hash_has_string_keys, rest: nil, key_rest: nil)
6
+ ::Etna::Injection.inject_new(self, hash ,hash_has_string_keys, rest: rest, key_rest: key_rest) do |missing_p|
7
+ raise "required argument '#{missing_p}' of #{self.name} is missing!"
8
+ end
9
+ end
10
+ end
11
+
12
+ def self.inject_new(cls, hash, hash_has_string_keys, rest: nil, key_rest: nil, &missing_req_param_cb)
13
+ args, kwds = prep_args(
14
+ cls.instance_method(:initialize), hash, hash_has_string_keys,
15
+ rest: rest, key_rest: key_rest, &missing_req_param_cb
16
+ )
17
+
18
+ cls.new(*args, **kwds)
19
+ end
20
+
21
+ def self.prep_args(method, hash, hash_has_string_keys, rest: nil, key_rest: nil, &missing_req_param_cb)
22
+ new_k_params = {}
23
+ new_p_params = []
24
+
25
+ method.parameters.each do |type, p_key|
26
+ h_key = hash_has_string_keys ? p_key.to_s : p_key
27
+ if type == :rest && rest
28
+ new_p_params.append(*rest)
29
+ elsif type == :keyrest && key_rest
30
+ new_k_params.update(key_rest)
31
+ elsif type == :req || type == :opt
32
+ if hash.include?(h_key)
33
+ new_p_params << hash[h_key]
34
+ elsif type == :req
35
+ if block_given?
36
+ new_p_params << missing_req_param_cb.call(h_key)
37
+ else
38
+ new_p_params << nil
39
+ end
40
+ end
41
+ elsif type == :keyreq || type == :key
42
+ if hash.include?(h_key)
43
+ new_k_params[p_key] = hash[h_key]
44
+ elsif type == :keyreq
45
+ if block_given?
46
+ new_k_params[p_key] = missing_req_param_cb.call(h_key)
47
+ else
48
+ new_k_params[p_key] = nil
49
+ end
50
+ end
51
+ end
52
+ end
53
+
54
+ [new_p_params, new_k_params]
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,55 @@
1
+ # Utility class to work with Janus for authz and authn
2
+ module Etna
3
+ class JanusUtils
4
+ def initialize
5
+ end
6
+
7
+ def projects(token)
8
+ return [] unless has_janus_config?
9
+
10
+ janus_client(token).get_projects.projects
11
+ rescue
12
+ # If encounter any issue with Janus, we'll return no projects
13
+ []
14
+ end
15
+
16
+ def resource_projects(token)
17
+ projects(token).select do |project|
18
+ !!project.resource && !project.requires_agreement
19
+ end
20
+ end
21
+
22
+ def community_projects(token)
23
+ projects(token).select do |project|
24
+ !!project.resource && !!project.requires_agreement
25
+ end
26
+ end
27
+
28
+ def janus_client(token)
29
+ Etna::Clients::Janus.new(
30
+ token: token,
31
+ host: application.config(:janus)[:host],
32
+ )
33
+ end
34
+
35
+ def valid_task_token?(token)
36
+ return false unless has_janus_config?
37
+
38
+ response = janus_client(token).validate_task_token
39
+
40
+ return false unless response.code == "200"
41
+
42
+ return true
43
+ end
44
+
45
+ private
46
+
47
+ def application
48
+ @application ||= Etna::Application.instance
49
+ end
50
+
51
+ def has_janus_config?
52
+ application.config(:janus) && application.config(:janus)[:host]
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,104 @@
1
+ module Etna
2
+ class Permissions
3
+ def initialize(permissions)
4
+ @permissions = permissions
5
+ end
6
+
7
+ def self.from_encoded_permissions(encoded_permissions)
8
+ perms = encoded_permissions.split(/\;/).map do |roles|
9
+ role, projects = roles.split(/:/)
10
+
11
+ projects.split(/\,/).reduce([]) do |perms, project_name|
12
+ perms << Etna::Permission.new(role, project_name)
13
+ end
14
+ end.flatten
15
+
16
+ Etna::Permissions.new(perms)
17
+ end
18
+
19
+ def self.from_hash(permissions_hash)
20
+ perms = permissions_hash.map do |project_name, role_hash|
21
+ Etna::Permission.new(
22
+ Etna::Role.new(role_hash[:role], role_hash[:restricted]).key,
23
+ project_name
24
+ )
25
+ end
26
+
27
+ Etna::Permissions.new(perms)
28
+ end
29
+
30
+ def to_string
31
+ @permissions_string ||= @permissions.group_by(&:role_key)
32
+ .sort_by(&:first)
33
+ .map do |role_key, permissions|
34
+ [
35
+ role_key,
36
+ permissions.map(&:project_name).sort.join(","),
37
+ ].join(":")
38
+ end.join(";")
39
+ end
40
+
41
+ def to_hash
42
+ @permissions_hash ||= @permissions.map do |permission|
43
+ [permission.project_name, permission.to_hash]
44
+ end.to_h
45
+ end
46
+
47
+ def add_permission(permission)
48
+ return if current_project_names.include?(permission.project_name)
49
+
50
+ @permissions << permission
51
+ end
52
+
53
+ def any?(&block)
54
+ @permissions.any?(&block)
55
+ end
56
+
57
+ private
58
+
59
+ def current_project_names
60
+ @permissions.map(&:project_name)
61
+ end
62
+ end
63
+
64
+ class Permission
65
+ attr_reader :role, :project_name, :role_key
66
+
67
+ ROLE_NAMES = {
68
+ "A" => :admin,
69
+ "E" => :editor,
70
+ "V" => :viewer,
71
+ "G" => :guest
72
+ }
73
+
74
+ def initialize(role_key, project_name)
75
+ @role_key = role_key
76
+ @role = Etna::Role.new(ROLE_NAMES[role_key.upcase], role_key == role_key.upcase)
77
+ @project_name = project_name
78
+ end
79
+
80
+ def to_hash
81
+ role.to_hash
82
+ end
83
+ end
84
+
85
+ class Role
86
+ attr_reader :role, :restricted
87
+ def initialize(role, restricted)
88
+ @role = role
89
+ @restricted = restricted
90
+ end
91
+
92
+ def key
93
+ role_key = role.to_s[0]
94
+ restricted ? role_key.upcase : role_key
95
+ end
96
+
97
+ def to_hash
98
+ {
99
+ role: role,
100
+ restricted: restricted,
101
+ }
102
+ end
103
+ end
104
+ end
@@ -0,0 +1,36 @@
1
+ module Etna
2
+ def self.Redirect(req)
3
+ Redirect.new(req)
4
+ end
5
+
6
+ class Redirect
7
+ def initialize(request)
8
+ @request = request
9
+ end
10
+
11
+ def to(path, &block)
12
+ return Rack::Response.new(
13
+ [ { errors: [ 'Cannot redirect out of domain' ] }.to_json ], 422,
14
+ { 'Content-Type' => 'application/json' }
15
+ ).finish unless matches_domain?(path)
16
+
17
+ response = Rack::Response.new
18
+
19
+ response.redirect(path.gsub("http://", "https://"), 302)
20
+
21
+ yield response if block_given?
22
+
23
+ response.finish
24
+ end
25
+
26
+ private
27
+
28
+ def matches_domain?(path)
29
+ top_domain(@request.hostname) == top_domain(URI(path).host)
30
+ end
31
+
32
+ def top_domain(host_name)
33
+ host_name.split('.')[-2..-1].join('.')
34
+ end
35
+ end
36
+ end