etna 0.1.16 → 0.1.18
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 +63 -0
- data/etna.completion +926 -0
- data/etna_app.completion +133 -0
- data/ext/completions/extconf.rb +20 -0
- data/lib/commands.rb +368 -0
- data/lib/etna.rb +6 -0
- data/lib/etna/application.rb +38 -20
- data/lib/etna/client.rb +60 -29
- 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 +2 -0
- data/lib/etna/clients/magma/client.rb +24 -9
- 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 +323 -9
- 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 +1 -0
- data/lib/etna/clients/metis/client.rb +207 -5
- data/lib/etna/clients/metis/models.rb +174 -3
- 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 +235 -5
- data/lib/etna/controller.rb +4 -0
- data/lib/etna/environment_scoped.rb +19 -0
- data/lib/etna/generate_autocompletion_script.rb +130 -0
- data/lib/etna/json_serializable_struct.rb +6 -3
- data/lib/etna/logger.rb +0 -3
- data/lib/etna/multipart_serializable_nested_hash.rb +6 -1
- data/lib/etna/route.rb +1 -1
- data/lib/etna/spec/vcr.rb +98 -0
- data/lib/etna/templates/attribute_actions_template.json +43 -0
- data/lib/etna/test_auth.rb +3 -1
- data/lib/etna/user.rb +4 -0
- data/lib/helpers.rb +81 -0
- metadata +47 -7
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,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,130 @@
|
|
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
|
+
generate_flag_handling
|
76
|
+
|
77
|
+
scope.subcommands.each do |name, command|
|
78
|
+
write %Q(elif [[ "$1" == "#{name}" ]]; then)
|
79
|
+
write 'shift'
|
80
|
+
if command.class.included_modules.include?(CommandExecutor)
|
81
|
+
generate_for_scope(command)
|
82
|
+
else
|
83
|
+
generate_for_command(command)
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
write "else"
|
88
|
+
write "return"
|
89
|
+
write "fi"
|
90
|
+
write 'done'
|
91
|
+
end
|
92
|
+
|
93
|
+
def program_name
|
94
|
+
$PROGRAM_NAME
|
95
|
+
end
|
96
|
+
|
97
|
+
def execute
|
98
|
+
name = File.basename(program_name)
|
99
|
+
|
100
|
+
write <<-EOF
|
101
|
+
#!/usr/bin/env bash
|
102
|
+
|
103
|
+
function _#{name}_completions() {
|
104
|
+
_#{name}_inner_completions "${COMP_WORDS[@]:1:COMP_CWORD}"
|
105
|
+
}
|
106
|
+
|
107
|
+
function _#{name}_inner_completions() {
|
108
|
+
local all_flag_completion_names=''
|
109
|
+
local string_flag_completion_names=''
|
110
|
+
local all_completion_names=''
|
111
|
+
local i=''
|
112
|
+
local a=''
|
113
|
+
EOF
|
114
|
+
generate_for_scope(parent)
|
115
|
+
write <<-EOF
|
116
|
+
}
|
117
|
+
|
118
|
+
complete -o default -F _#{name}_completions #{name}
|
119
|
+
EOF
|
120
|
+
|
121
|
+
File.open("#{name}.completion", 'w') { |f| f.write(@script) }
|
122
|
+
end
|
123
|
+
|
124
|
+
def write(string)
|
125
|
+
@script ||= ""
|
126
|
+
@script << string
|
127
|
+
@script << "\n"
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
@@ -20,11 +20,14 @@ module Etna
|
|
20
20
|
end
|
21
21
|
end
|
22
22
|
|
23
|
-
def as_json
|
24
|
-
members.map do |k|
|
23
|
+
def as_json(keep_nils: false)
|
24
|
+
inner_json = members.map do |k|
|
25
25
|
v = self.class.as_json(send(k))
|
26
26
|
[k, v]
|
27
|
-
end.to_h
|
27
|
+
end.to_h
|
28
|
+
|
29
|
+
return inner_json if keep_nils
|
30
|
+
inner_json.delete_if { |k, v| v.nil? }
|
28
31
|
end
|
29
32
|
|
30
33
|
def to_json
|
data/lib/etna/logger.rb
CHANGED
@@ -16,17 +16,14 @@ module Etna
|
|
16
16
|
|
17
17
|
def warn(msg, &block)
|
18
18
|
super
|
19
|
-
Rollbar.warn(msg)
|
20
19
|
end
|
21
20
|
|
22
21
|
def error(msg, &block)
|
23
22
|
super
|
24
|
-
Rollbar.error(msg)
|
25
23
|
end
|
26
24
|
|
27
25
|
def fatal(msg, &block)
|
28
26
|
super
|
29
|
-
Rollbar.error(msg)
|
30
27
|
end
|
31
28
|
|
32
29
|
def log_error(e)
|
@@ -20,7 +20,12 @@ module Etna
|
|
20
20
|
end
|
21
21
|
else
|
22
22
|
raise "base_key cannot be empty for a scalar value!" if base_key.length == 0
|
23
|
-
|
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
|
24
29
|
end
|
25
30
|
end
|
26
31
|
|
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)
|
@@ -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
|
@@ -0,0 +1,43 @@
|
|
1
|
+
// Make sure to remove all comments before using -- JSON doesn't allow comments!
|
2
|
+
[
|
3
|
+
// Attribute actions can be "add_attribute", "update_attribute", "rename_attribute", or "add_link".
|
4
|
+
// These are all executed at the same time -- so you can't "update" an attribute that you "add"
|
5
|
+
// in the same file -- everything must already exist on the server."
|
6
|
+
{
|
7
|
+
"action_name": "add_attribute",
|
8
|
+
"model_name": "assay_name", // Each action must include a model_name that it applies to.
|
9
|
+
"attribute_type": "string", // When adding, you must include attribute_type, attribute_name, desc, and display_name.
|
10
|
+
"attribute_name": "notes",
|
11
|
+
"display_name": "Notes",
|
12
|
+
"desc": "for notes that you have."
|
13
|
+
},
|
14
|
+
{
|
15
|
+
"action_name": "update_attribute",
|
16
|
+
"model_name": "document",
|
17
|
+
"attribute_name": "version", // When updating, you must include model_name and attribute_name, but any additional values are optional.
|
18
|
+
"read_only": true
|
19
|
+
},
|
20
|
+
{
|
21
|
+
"action_name": "rename_attribute",
|
22
|
+
"model_name": "assay_name",
|
23
|
+
"attribute_name": "vendor",
|
24
|
+
"new_attribute_name": "e_vendor" // When renaming an attribute, you need to include a "new_attribute_name" value.
|
25
|
+
},
|
26
|
+
{
|
27
|
+
// Links require two hashes inside of a "links" attribute.
|
28
|
+
"action_name": "add_link",
|
29
|
+
"links": [
|
30
|
+
{
|
31
|
+
"model_name": "assay_name",
|
32
|
+
"attribute_name": "document",
|
33
|
+
"attribute_type": "link" // One link must be of type "link". This has a one-to-one relationship with the other model.
|
34
|
+
// In this case, each assay_name has one document record, but document records can point to zero-or-more assay_name records.
|
35
|
+
},
|
36
|
+
{
|
37
|
+
"model_name": "document",
|
38
|
+
"attribute_name": "assay_name",
|
39
|
+
"attribute_type": "collection" // The other link must be of type "collection". This is a one-to-many relationship with the other model.
|
40
|
+
}
|
41
|
+
]
|
42
|
+
}
|
43
|
+
]
|
data/lib/etna/test_auth.rb
CHANGED
@@ -43,7 +43,9 @@ module Etna
|
|
43
43
|
end
|
44
44
|
|
45
45
|
def approve_hmac(request)
|
46
|
-
hmac_signature = etna_param(request, :signature)
|
46
|
+
hmac_signature = etna_param(request, :signature)
|
47
|
+
|
48
|
+
return false unless hmac_signature
|
47
49
|
|
48
50
|
headers = (etna_param(request, :headers)&.split(/,/) || []).map do |header|
|
49
51
|
[ header.to_sym, etna_param(request, header) ]
|
data/lib/etna/user.rb
CHANGED
data/lib/helpers.rb
ADDED
@@ -0,0 +1,81 @@
|
|
1
|
+
require_relative './etna/clients'
|
2
|
+
require_relative './etna/environment_scoped'
|
3
|
+
|
4
|
+
module WithEtnaClients
|
5
|
+
def environment
|
6
|
+
EtnaApp.instance.environment
|
7
|
+
end
|
8
|
+
|
9
|
+
def token
|
10
|
+
env_token = ENV['TOKEN']
|
11
|
+
if !env_token
|
12
|
+
puts "No environment variable TOKEN is set. You should set your token with `export TOKEN=<your.janus.token>` before running."
|
13
|
+
redirect = EtnaApp.instance.config(:auth_redirect)
|
14
|
+
|
15
|
+
if redirect.nil? && EtnaApp.instance.environment == :production
|
16
|
+
redirect = 'https://janus.ucsf.edu/'
|
17
|
+
end
|
18
|
+
|
19
|
+
unless redirect.nil?
|
20
|
+
puts "Open your browser to #{redirect} to complete login and copy your token."
|
21
|
+
end
|
22
|
+
|
23
|
+
exit
|
24
|
+
end
|
25
|
+
|
26
|
+
env_token
|
27
|
+
end
|
28
|
+
|
29
|
+
def magma_client
|
30
|
+
@magma_client ||= Etna::Clients::Magma.new(
|
31
|
+
token: token,
|
32
|
+
ignore_ssl: EtnaApp.instance.config(:ignore_ssl),
|
33
|
+
# Persistent connections cause problem with magma restarts, until we can fix that we should force them
|
34
|
+
# to close + reopen each request.
|
35
|
+
persistent: false,
|
36
|
+
**EtnaApp.instance.config(:magma, environment) || {})
|
37
|
+
end
|
38
|
+
|
39
|
+
def metis_client
|
40
|
+
@metis_client ||= Etna::Clients::Metis.new(
|
41
|
+
token: token,
|
42
|
+
ignore_ssl: EtnaApp.instance.config(:ignore_ssl),
|
43
|
+
**EtnaApp.instance.config(:metis, environment) || {})
|
44
|
+
end
|
45
|
+
|
46
|
+
def janus_client
|
47
|
+
@janus_client ||= Etna::Clients::Janus.new(
|
48
|
+
token: token,
|
49
|
+
ignore_ssl: EtnaApp.instance.config(:ignore_ssl),
|
50
|
+
**EtnaApp.instance.config(:janus, environment) || {})
|
51
|
+
end
|
52
|
+
|
53
|
+
def polyphemus_client
|
54
|
+
@polyphemus_client ||= Etna::Clients::Polyphemus.new(
|
55
|
+
token: token,
|
56
|
+
ignore_ssl: EtnaApp.instance.config(:ignore_ssl),
|
57
|
+
**EtnaApp.instance.config(:polyphemus, environment) || {})
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
module WithLogger
|
62
|
+
def logger
|
63
|
+
EtnaApp.instance.logger
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
module StrongConfirmation
|
68
|
+
def confirm
|
69
|
+
puts "Confirm Y/n:"
|
70
|
+
input = STDIN.gets.chomp
|
71
|
+
if input != "Y"
|
72
|
+
return false
|
73
|
+
end
|
74
|
+
|
75
|
+
true
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
WithEtnaClientsByEnvironment = EnvironmentScoped.new do
|
80
|
+
include WithEtnaClients
|
81
|
+
end
|