etna 0.1.13 → 0.1.19
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.
- checksums.yaml +4 -4
- data/bin/etna +18 -0
- data/etna.completion +926 -0
- data/etna_app.completion +133 -0
- data/ext/completions/extconf.rb +20 -0
- data/lib/commands.rb +360 -0
- data/lib/etna.rb +6 -0
- data/lib/etna/application.rb +46 -22
- data/lib/etna/client.rb +82 -48
- data/lib/etna/clients.rb +4 -0
- data/lib/etna/clients/enum.rb +9 -0
- data/lib/etna/clients/janus.rb +2 -0
- data/lib/etna/clients/janus/client.rb +73 -0
- data/lib/etna/clients/janus/models.rb +78 -0
- data/lib/etna/clients/magma.rb +4 -0
- data/lib/etna/clients/magma/client.rb +80 -0
- data/lib/etna/clients/magma/formatting.rb +1 -0
- data/lib/etna/clients/magma/formatting/models_csv.rb +345 -0
- data/lib/etna/clients/magma/models.rb +579 -0
- data/lib/etna/clients/magma/workflows.rb +10 -0
- data/lib/etna/clients/magma/workflows/add_project_models_workflow.rb +78 -0
- data/lib/etna/clients/magma/workflows/attribute_actions_from_json_workflow.rb +62 -0
- data/lib/etna/clients/magma/workflows/create_project_workflow.rb +117 -0
- data/lib/etna/clients/magma/workflows/crud_workflow.rb +85 -0
- data/lib/etna/clients/magma/workflows/ensure_containing_record_workflow.rb +44 -0
- data/lib/etna/clients/magma/workflows/file_attributes_blank_workflow.rb +68 -0
- data/lib/etna/clients/magma/workflows/file_linking_workflow.rb +115 -0
- data/lib/etna/clients/magma/workflows/json_converters.rb +81 -0
- data/lib/etna/clients/magma/workflows/json_validators.rb +447 -0
- data/lib/etna/clients/magma/workflows/model_synchronization_workflow.rb +306 -0
- data/lib/etna/clients/magma/workflows/record_synchronization_workflow.rb +63 -0
- data/lib/etna/clients/magma/workflows/update_attributes_from_csv_workflow.rb +178 -0
- data/lib/etna/clients/metis.rb +3 -0
- data/lib/etna/clients/metis/client.rb +239 -0
- data/lib/etna/clients/metis/models.rb +313 -0
- data/lib/etna/clients/metis/workflows.rb +2 -0
- data/lib/etna/clients/metis/workflows/metis_download_workflow.rb +37 -0
- data/lib/etna/clients/metis/workflows/metis_upload_workflow.rb +137 -0
- data/lib/etna/clients/polyphemus.rb +3 -0
- data/lib/etna/clients/polyphemus/client.rb +33 -0
- data/lib/etna/clients/polyphemus/models.rb +68 -0
- data/lib/etna/clients/polyphemus/workflows.rb +1 -0
- data/lib/etna/clients/polyphemus/workflows/set_configuration_workflow.rb +47 -0
- data/lib/etna/command.rb +243 -5
- data/lib/etna/controller.rb +4 -0
- data/lib/etna/directed_graph.rb +56 -0
- data/lib/etna/environment_scoped.rb +19 -0
- data/lib/etna/generate_autocompletion_script.rb +131 -0
- data/lib/etna/hmac.rb +1 -0
- data/lib/etna/json_serializable_struct.rb +37 -0
- data/lib/etna/logger.rb +15 -1
- data/lib/etna/multipart_serializable_nested_hash.rb +50 -0
- data/lib/etna/parse_body.rb +1 -1
- data/lib/etna/route.rb +1 -1
- data/lib/etna/server.rb +3 -0
- data/lib/etna/spec/vcr.rb +98 -0
- data/lib/etna/templates/attribute_actions_template.json +43 -0
- data/lib/etna/test_auth.rb +4 -2
- data/lib/etna/user.rb +11 -1
- data/lib/helpers.rb +87 -0
- metadata +69 -5
data/lib/etna/controller.rb
CHANGED
@@ -12,6 +12,7 @@ module Etna
|
|
12
12
|
@logger = @request.env['etna.logger']
|
13
13
|
@user = @request.env['etna.user']
|
14
14
|
@request_id = @request.env['etna.request_id']
|
15
|
+
@hmac = @request.env['etna.hmac']
|
15
16
|
end
|
16
17
|
|
17
18
|
def log(line)
|
@@ -23,11 +24,14 @@ module Etna
|
|
23
24
|
|
24
25
|
return send(@action) if @action
|
25
26
|
|
27
|
+
|
26
28
|
[501, {}, ['This controller is not implemented.']]
|
27
29
|
rescue Etna::Error => e
|
30
|
+
Rollbar.error(e)
|
28
31
|
@logger.error(request_msg("Exiting with #{e.status}, #{e.message}"))
|
29
32
|
return failure(e.status, error: e.message)
|
30
33
|
rescue Exception => e
|
34
|
+
Rollbar.error(e)
|
31
35
|
@logger.error(request_msg('Caught unspecified error'))
|
32
36
|
@logger.error(request_msg(e.message))
|
33
37
|
e.backtrace.each do |trace|
|
@@ -0,0 +1,56 @@
|
|
1
|
+
class DirectedGraph
|
2
|
+
def initialize
|
3
|
+
@children = {}
|
4
|
+
@parents = {}
|
5
|
+
end
|
6
|
+
|
7
|
+
attr_reader :children
|
8
|
+
attr_reader :parents
|
9
|
+
|
10
|
+
def add_connection(parent, child)
|
11
|
+
children = @children[parent] ||= {}
|
12
|
+
child_children = @children[child] ||= {}
|
13
|
+
|
14
|
+
children[child] = child_children
|
15
|
+
|
16
|
+
parents = @parents[child] ||= {}
|
17
|
+
parent_parents = @parents[parent] ||= {}
|
18
|
+
parents[parent] = parent_parents
|
19
|
+
end
|
20
|
+
|
21
|
+
def descendants(parent)
|
22
|
+
seen = Set.new
|
23
|
+
|
24
|
+
seen.add(parent)
|
25
|
+
queue = @children[parent].keys.dup
|
26
|
+
parent_queue = @parents[parent].keys.dup
|
27
|
+
|
28
|
+
# Because this is not an acyclical graph, the definition of descendants needs to be stronger;
|
29
|
+
# here we believe that any path that would move through --any-- parent to this child would not be considered
|
30
|
+
# descendant, so we first find all those parents and mark them as 'seen' so that they are not traveled.
|
31
|
+
while next_parent = parent_queue.pop
|
32
|
+
next if seen.include?(next_parent)
|
33
|
+
seen.add(next_parent)
|
34
|
+
parent_queue.push(*@parents[next_parent].keys)
|
35
|
+
end
|
36
|
+
|
37
|
+
queue = queue.nil? ? [] : queue.dup
|
38
|
+
paths = {}
|
39
|
+
|
40
|
+
while child = queue.pop
|
41
|
+
next if seen.include? child
|
42
|
+
seen.add(child)
|
43
|
+
path = (paths[child] ||= [parent])
|
44
|
+
|
45
|
+
@children[child].keys.each do |child_child|
|
46
|
+
queue.push child_child
|
47
|
+
|
48
|
+
unless paths.include? child_child
|
49
|
+
paths[child_child] = path + [child]
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
paths
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
class EnvironmentScoped < Module
|
2
|
+
def initialize(&block)
|
3
|
+
environment_class = Class.new do
|
4
|
+
class_eval(&block)
|
5
|
+
|
6
|
+
attr_reader :environment
|
7
|
+
def initialize(environment)
|
8
|
+
@environment = environment
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
super() do
|
13
|
+
define_method :environment do |env|
|
14
|
+
env = env.to_sym
|
15
|
+
(@envs ||= {})[env] ||= environment_class.new(env)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,131 @@
|
|
1
|
+
require_relative 'command'
|
2
|
+
|
3
|
+
# application.rb instantiates this for the project scoping.
|
4
|
+
# This generates a file, project-name.completion, which is sourced
|
5
|
+
# in build.sh to provide autocompletion in that environment.
|
6
|
+
module Etna
|
7
|
+
class GenerateCompletionScript < Etna::Command
|
8
|
+
def generate_for_command(command)
|
9
|
+
completions = command.completions
|
10
|
+
completions.each do |c|
|
11
|
+
generate_start_match(c, false)
|
12
|
+
write "fi"
|
13
|
+
write "shift"
|
14
|
+
end
|
15
|
+
|
16
|
+
enable_flags(command.class)
|
17
|
+
write 'while [[ "$#" != "0" ]]; do'
|
18
|
+
generate_start_match([])
|
19
|
+
generate_flag_handling
|
20
|
+
|
21
|
+
write "else"
|
22
|
+
write "return"
|
23
|
+
write 'fi'
|
24
|
+
write 'done'
|
25
|
+
write "return"
|
26
|
+
end
|
27
|
+
|
28
|
+
def generate_flag_handling
|
29
|
+
write %Q(elif [[ -z "$(echo $all_flag_completion_names | xargs)" ]]; then)
|
30
|
+
write "return"
|
31
|
+
write %Q(elif [[ "$all_flag_completion_names" =~ $1\\ ]]; then)
|
32
|
+
write %Q(all_flag_completion_names="${all_flag_completion_names//$1\\ /}")
|
33
|
+
write 'a=$1'
|
34
|
+
write 'shift'
|
35
|
+
write %Q(if [[ "$string_flag_completion_names" =~ $a\\ ]]; then)
|
36
|
+
write 'if [[ "$#" == "1" ]]; then'
|
37
|
+
write %Q(a="${a//--/}")
|
38
|
+
write %Q(a="${a//-/_}")
|
39
|
+
write %Q(i="_completions_for_$a")
|
40
|
+
write %Q(all_completion_names="${!i}")
|
41
|
+
write 'COMPREPLY=($(compgen -W "$all_completion_names" -- "$1"))'
|
42
|
+
write 'return'
|
43
|
+
write 'fi'
|
44
|
+
write 'shift'
|
45
|
+
write 'fi'
|
46
|
+
end
|
47
|
+
|
48
|
+
def generate_start_match(completions, include_flags=true)
|
49
|
+
write 'if [[ "$#" == "1" ]]; then'
|
50
|
+
write %Q(all_completion_names="#{completions.join(' ')}")
|
51
|
+
write %Q(all_completion_names="$all_completion_names $all_flag_completion_names") if include_flags
|
52
|
+
write %Q(if [[ -z "$(echo $all_completion_names | xargs)" ]]; then)
|
53
|
+
write 'return'
|
54
|
+
write 'fi'
|
55
|
+
write 'COMPREPLY=($(compgen -W "$all_completion_names" -- "$1"))'
|
56
|
+
write 'return'
|
57
|
+
end
|
58
|
+
|
59
|
+
def enable_flags(flags_container)
|
60
|
+
boolean_flags = flags_container.boolean_flags
|
61
|
+
string_flags = flags_container.string_flags
|
62
|
+
flags = boolean_flags + string_flags
|
63
|
+
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(' ')} ")
|
65
|
+
|
66
|
+
string_flags.each do |flag|
|
67
|
+
write %Q(declare _completions_for_#{flag_as_parameter(flag)}="#{completions_for(flag_as_parameter(flag)).join(' ')}")
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def generate_for_scope(scope)
|
72
|
+
enable_flags(scope.class)
|
73
|
+
write 'while [[ "$#" != "0" ]]; do'
|
74
|
+
generate_start_match(scope.subcommands.keys)
|
75
|
+
|
76
|
+
scope.subcommands.each do |name, command|
|
77
|
+
write %Q(elif [[ "$1" == "#{name}" ]]; then)
|
78
|
+
write 'shift'
|
79
|
+
if command.class.included_modules.include?(CommandExecutor)
|
80
|
+
generate_for_scope(command)
|
81
|
+
else
|
82
|
+
generate_for_command(command)
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
generate_flag_handling
|
87
|
+
|
88
|
+
write "else"
|
89
|
+
write "return"
|
90
|
+
write "fi"
|
91
|
+
write 'done'
|
92
|
+
end
|
93
|
+
|
94
|
+
def program_name
|
95
|
+
$PROGRAM_NAME
|
96
|
+
end
|
97
|
+
|
98
|
+
def execute
|
99
|
+
name = File.basename(program_name)
|
100
|
+
|
101
|
+
write <<-EOF
|
102
|
+
#!/usr/bin/env bash
|
103
|
+
|
104
|
+
function _#{name}_completions() {
|
105
|
+
_#{name}_inner_completions "${COMP_WORDS[@]:1:COMP_CWORD}"
|
106
|
+
}
|
107
|
+
|
108
|
+
function _#{name}_inner_completions() {
|
109
|
+
local all_flag_completion_names=''
|
110
|
+
local string_flag_completion_names=''
|
111
|
+
local all_completion_names=''
|
112
|
+
local i=''
|
113
|
+
local a=''
|
114
|
+
EOF
|
115
|
+
generate_for_scope(parent)
|
116
|
+
write <<-EOF
|
117
|
+
}
|
118
|
+
|
119
|
+
complete -o default -F _#{name}_completions #{name}
|
120
|
+
EOF
|
121
|
+
|
122
|
+
File.open("#{name}.completion", 'w') { |f| f.write(@script) }
|
123
|
+
end
|
124
|
+
|
125
|
+
def write(string)
|
126
|
+
@script ||= ""
|
127
|
+
@script << string
|
128
|
+
@script << "\n"
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
data/lib/etna/hmac.rb
CHANGED
@@ -0,0 +1,37 @@
|
|
1
|
+
module Etna
|
2
|
+
module JsonSerializableStruct
|
3
|
+
def self.included(cls)
|
4
|
+
cls.instance_eval do
|
5
|
+
def self.as_json(v)
|
6
|
+
if v.respond_to? :as_json
|
7
|
+
return v.as_json
|
8
|
+
end
|
9
|
+
|
10
|
+
if v.is_a? Hash
|
11
|
+
return v.map { |k, v| [k, as_json(v)] }.to_h
|
12
|
+
end
|
13
|
+
|
14
|
+
if v.class.include? Enumerable
|
15
|
+
return v.map { |v| as_json(v) }
|
16
|
+
end
|
17
|
+
|
18
|
+
v
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def as_json(keep_nils: false)
|
24
|
+
inner_json = members.map do |k|
|
25
|
+
v = self.class.as_json(send(k))
|
26
|
+
[k, v]
|
27
|
+
end.to_h
|
28
|
+
|
29
|
+
return inner_json if keep_nils
|
30
|
+
inner_json.delete_if { |k, v| v.nil? }
|
31
|
+
end
|
32
|
+
|
33
|
+
def to_json
|
34
|
+
as_json.to_json
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
data/lib/etna/logger.rb
CHANGED
@@ -1,10 +1,10 @@
|
|
1
1
|
require 'logger'
|
2
|
+
require 'rollbar'
|
2
3
|
|
3
4
|
module Etna
|
4
5
|
class Logger < ::Logger
|
5
6
|
def initialize(log_dev, age, size)
|
6
7
|
super
|
7
|
-
|
8
8
|
self.formatter = proc do |severity, datetime, progname, msg|
|
9
9
|
format(severity, datetime, progname, msg)
|
10
10
|
end
|
@@ -14,11 +14,25 @@ module Etna
|
|
14
14
|
"#{severity}:#{datetime.iso8601} #{msg}\n"
|
15
15
|
end
|
16
16
|
|
17
|
+
def warn(msg, &block)
|
18
|
+
super
|
19
|
+
end
|
20
|
+
|
21
|
+
def error(msg, &block)
|
22
|
+
super
|
23
|
+
end
|
24
|
+
|
25
|
+
def fatal(msg, &block)
|
26
|
+
super
|
27
|
+
end
|
28
|
+
|
17
29
|
def log_error(e)
|
18
30
|
error(e.message)
|
19
31
|
e.backtrace.each do |trace|
|
20
32
|
error(trace)
|
21
33
|
end
|
34
|
+
|
35
|
+
Rollbar.error(e)
|
22
36
|
end
|
23
37
|
|
24
38
|
def log_request(request)
|
@@ -0,0 +1,50 @@
|
|
1
|
+
module Etna
|
2
|
+
module MultipartSerializableNestedHash
|
3
|
+
def self.included(cls)
|
4
|
+
cls.instance_eval do
|
5
|
+
def self.encode_multipart_pairs(value, base_key, is_root, &block)
|
6
|
+
if value.is_a? Hash
|
7
|
+
value.each do |k, v|
|
8
|
+
encode_multipart_pairs(v, is_root ? k : "#{base_key}[#{k}]", false, &block)
|
9
|
+
end
|
10
|
+
elsif value.is_a? Array
|
11
|
+
value.each_with_index do |v, i|
|
12
|
+
# This is necessary to ensure that arrays of hashes that have hetergenous keys still get parsed correctly
|
13
|
+
# Since the only way to indicate a new entry in the array of hashes is by re-using a key that existed in
|
14
|
+
# the previous hash.
|
15
|
+
if v.is_a? Hash
|
16
|
+
encode_multipart_pairs(i, "#{base_key}[][_idx]", false, &block)
|
17
|
+
end
|
18
|
+
|
19
|
+
encode_multipart_pairs(v, "#{base_key}[]", false, &block)
|
20
|
+
end
|
21
|
+
else
|
22
|
+
raise "base_key cannot be empty for a scalar value!" if base_key.length == 0
|
23
|
+
|
24
|
+
if value.respond_to?(:read)
|
25
|
+
yield [base_key, UploadIO.new(value, 'application/octet-stream'), {filename: 'blob'}]
|
26
|
+
else
|
27
|
+
yield [base_key, value.to_s]
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
|
33
|
+
def self.encode_multipart_content(value, base_key = '', is_root = true)
|
34
|
+
result = []
|
35
|
+
self.encode_multipart_pairs(value, base_key, is_root) { |pair| result << pair }
|
36
|
+
result
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def encode_multipart_content(base_key = '')
|
42
|
+
value = self
|
43
|
+
if value.respond_to? :as_json
|
44
|
+
value = value.as_json
|
45
|
+
end
|
46
|
+
|
47
|
+
self.class.encode_multipart_content(value, base_key)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
data/lib/etna/parse_body.rb
CHANGED
data/lib/etna/route.rb
CHANGED
@@ -118,7 +118,7 @@ module Etna
|
|
118
118
|
|
119
119
|
def hmac_authorized?(request)
|
120
120
|
# either there is no hmac requirement, or we have a valid hmac
|
121
|
-
!@auth[:hmac] || request.env['etna.hmac']
|
121
|
+
!@auth[:hmac] || request.env['etna.hmac']&.valid?
|
122
122
|
end
|
123
123
|
|
124
124
|
def route_name(options)
|
data/lib/etna/server.rb
CHANGED
@@ -0,0 +1,98 @@
|
|
1
|
+
require 'webmock/rspec'
|
2
|
+
require 'vcr'
|
3
|
+
require 'openssl'
|
4
|
+
require 'digest/sha2'
|
5
|
+
require 'base64'
|
6
|
+
|
7
|
+
def setup_base_vcr(spec_helper_dir)
|
8
|
+
VCR.configure do |c|
|
9
|
+
c.hook_into :webmock
|
10
|
+
c.cassette_serializers
|
11
|
+
c.cassette_library_dir = ::File.join(spec_helper_dir, 'fixtures', 'cassettes')
|
12
|
+
c.allow_http_connections_when_no_cassette = true
|
13
|
+
|
14
|
+
c.register_request_matcher :try_body do |request_1, request_2|
|
15
|
+
if request_1.headers['Content-Type'].first =~ /application\/json/
|
16
|
+
if request_2.headers['Content-Type'].first =~ /application\/json/
|
17
|
+
request_1_json = begin
|
18
|
+
JSON.parse(request_1.body) rescue 'not-json'
|
19
|
+
end
|
20
|
+
request_2_json = begin
|
21
|
+
JSON.parse(request_2.body) rescue 'not-json'
|
22
|
+
end
|
23
|
+
request_1_json == request_2_json
|
24
|
+
else
|
25
|
+
false
|
26
|
+
end
|
27
|
+
else
|
28
|
+
request_1.body == request_2.body
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
c.default_cassette_options = {
|
33
|
+
serialize_with: :compressed,
|
34
|
+
record: if ENV['IS_CI'] == '1'
|
35
|
+
:none
|
36
|
+
else
|
37
|
+
ENV['RERECORD'] ? :all : :once
|
38
|
+
end,
|
39
|
+
match_requests_on: [:method, :uri, :try_body]
|
40
|
+
}
|
41
|
+
|
42
|
+
# Filter the authorization headers of any request by replacing any occurrence of that request's
|
43
|
+
# Authorization value with <AUTHORIZATION>
|
44
|
+
c.filter_sensitive_data('<AUTHORIZATION>') do |interaction|
|
45
|
+
interaction.request.headers['Authorization'].first
|
46
|
+
end
|
47
|
+
|
48
|
+
c.before_record do |interaction|
|
49
|
+
key = prepare_vcr_secret
|
50
|
+
|
51
|
+
if interaction.response.body && !interaction.response.body.empty?
|
52
|
+
cipher = OpenSSL::Cipher.new("AES-256-CBC")
|
53
|
+
iv = cipher.random_iv
|
54
|
+
|
55
|
+
cipher.encrypt
|
56
|
+
cipher.key = key
|
57
|
+
cipher.iv = iv
|
58
|
+
|
59
|
+
encrypted = cipher.update(interaction.response.body)
|
60
|
+
encrypted << cipher.final
|
61
|
+
|
62
|
+
interaction.response.body = [iv, encrypted].pack('mm')
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
c.before_playback do |interaction|
|
67
|
+
key = prepare_vcr_secret
|
68
|
+
|
69
|
+
if interaction.response.body && !interaction.response.body.empty?
|
70
|
+
iv, encrypted = interaction.response.body.unpack('mm')
|
71
|
+
|
72
|
+
cipher = OpenSSL::Cipher.new("AES-256-CBC")
|
73
|
+
cipher.decrypt
|
74
|
+
cipher.key = key
|
75
|
+
cipher.iv = iv
|
76
|
+
|
77
|
+
plain = cipher.update(encrypted)
|
78
|
+
plain << cipher.final
|
79
|
+
|
80
|
+
interaction.response.body = plain
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
def prepare_vcr_secret
|
87
|
+
secret = ENV["CI_SECRET"]
|
88
|
+
|
89
|
+
if (secret.nil? || secret.empty?) && ENV['IS_CI'] != '1'
|
90
|
+
current_example = RSpec.current_example
|
91
|
+
RSpec::Core::Pending.mark_pending! current_example, 'CI_SECRET must be set to run this test'
|
92
|
+
raise "CI_SECRET must be set to run this test"
|
93
|
+
end
|
94
|
+
|
95
|
+
digest = Digest::SHA256.new
|
96
|
+
digest.update(secret)
|
97
|
+
digest.digest
|
98
|
+
end
|