etna 0.1.12 → 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.
Files changed (61) hide show
  1. checksums.yaml +4 -4
  2. data/bin/etna +63 -0
  3. data/etna.completion +926 -0
  4. data/etna_app.completion +133 -0
  5. data/ext/completions/extconf.rb +20 -0
  6. data/lib/commands.rb +368 -0
  7. data/lib/etna.rb +6 -0
  8. data/lib/etna/application.rb +46 -22
  9. data/lib/etna/client.rb +82 -48
  10. data/lib/etna/clients.rb +4 -0
  11. data/lib/etna/clients/enum.rb +9 -0
  12. data/lib/etna/clients/janus.rb +2 -0
  13. data/lib/etna/clients/janus/client.rb +73 -0
  14. data/lib/etna/clients/janus/models.rb +78 -0
  15. data/lib/etna/clients/magma.rb +4 -0
  16. data/lib/etna/clients/magma/client.rb +80 -0
  17. data/lib/etna/clients/magma/formatting.rb +1 -0
  18. data/lib/etna/clients/magma/formatting/models_csv.rb +345 -0
  19. data/lib/etna/clients/magma/models.rb +579 -0
  20. data/lib/etna/clients/magma/workflows.rb +10 -0
  21. data/lib/etna/clients/magma/workflows/add_project_models_workflow.rb +78 -0
  22. data/lib/etna/clients/magma/workflows/attribute_actions_from_json_workflow.rb +62 -0
  23. data/lib/etna/clients/magma/workflows/create_project_workflow.rb +117 -0
  24. data/lib/etna/clients/magma/workflows/crud_workflow.rb +85 -0
  25. data/lib/etna/clients/magma/workflows/ensure_containing_record_workflow.rb +44 -0
  26. data/lib/etna/clients/magma/workflows/file_attributes_blank_workflow.rb +68 -0
  27. data/lib/etna/clients/magma/workflows/file_linking_workflow.rb +115 -0
  28. data/lib/etna/clients/magma/workflows/json_converters.rb +81 -0
  29. data/lib/etna/clients/magma/workflows/json_validators.rb +447 -0
  30. data/lib/etna/clients/magma/workflows/model_synchronization_workflow.rb +306 -0
  31. data/lib/etna/clients/magma/workflows/record_synchronization_workflow.rb +63 -0
  32. data/lib/etna/clients/magma/workflows/update_attributes_from_csv_workflow.rb +178 -0
  33. data/lib/etna/clients/metis.rb +3 -0
  34. data/lib/etna/clients/metis/client.rb +239 -0
  35. data/lib/etna/clients/metis/models.rb +313 -0
  36. data/lib/etna/clients/metis/workflows.rb +2 -0
  37. data/lib/etna/clients/metis/workflows/metis_download_workflow.rb +37 -0
  38. data/lib/etna/clients/metis/workflows/metis_upload_workflow.rb +137 -0
  39. data/lib/etna/clients/polyphemus.rb +3 -0
  40. data/lib/etna/clients/polyphemus/client.rb +33 -0
  41. data/lib/etna/clients/polyphemus/models.rb +68 -0
  42. data/lib/etna/clients/polyphemus/workflows.rb +1 -0
  43. data/lib/etna/clients/polyphemus/workflows/set_configuration_workflow.rb +47 -0
  44. data/lib/etna/command.rb +243 -5
  45. data/lib/etna/controller.rb +4 -0
  46. data/lib/etna/directed_graph.rb +56 -0
  47. data/lib/etna/environment_scoped.rb +19 -0
  48. data/lib/etna/generate_autocompletion_script.rb +130 -0
  49. data/lib/etna/hmac.rb +1 -0
  50. data/lib/etna/json_serializable_struct.rb +37 -0
  51. data/lib/etna/logger.rb +15 -1
  52. data/lib/etna/multipart_serializable_nested_hash.rb +50 -0
  53. data/lib/etna/parse_body.rb +1 -1
  54. data/lib/etna/route.rb +1 -1
  55. data/lib/etna/server.rb +3 -0
  56. data/lib/etna/spec/vcr.rb +98 -0
  57. data/lib/etna/templates/attribute_actions_template.json +43 -0
  58. data/lib/etna/test_auth.rb +4 -2
  59. data/lib/etna/user.rb +11 -1
  60. data/lib/helpers.rb +81 -0
  61. metadata +70 -7
@@ -0,0 +1,133 @@
1
+ #!/usr/bin/env bash
2
+ _etna_app_completions() {
3
+ local all_completion_names=''
4
+ if [ "${COMP_CWORD}" == "1" ]; then
5
+ all_completion_names='add_model attribute_actions attribute_actions_template console create_project generate_completion_script help model_template project_template update_attributes_from_csv validate_attribute_actions validate_model validate_project'
6
+ COMPREPLY=($(compgen -W "$all_completion_names" "${COMP_WORDS[COMP_CWORD]}"))
7
+ else
8
+ if [ "${COMP_WORDS[1]}" == 'add_model' ]; then
9
+ if [ "${COMP_CWORD}" == "2" ]; then
10
+ COMPREPLY=()
11
+ return
12
+ fi
13
+ if [ "${COMP_CWORD}" == "3" ]; then
14
+ COMPREPLY=()
15
+ return
16
+ fi
17
+ if [ "${COMP_CWORD}" == "4" ]; then
18
+ COMPREPLY=()
19
+ return
20
+ fi
21
+ if [ "${COMP_CWORD}" == "5" ]; then
22
+ COMPREPLY=()
23
+ return
24
+ fi
25
+ true
26
+ fi
27
+ if [ "${COMP_WORDS[1]}" == 'attribute_actions' ]; then
28
+ if [ "${COMP_CWORD}" == "2" ]; then
29
+ COMPREPLY=()
30
+ return
31
+ fi
32
+ if [ "${COMP_CWORD}" == "3" ]; then
33
+ COMPREPLY=()
34
+ return
35
+ fi
36
+ if [ "${COMP_CWORD}" == "4" ]; then
37
+ COMPREPLY=()
38
+ return
39
+ fi
40
+ true
41
+ fi
42
+ if [ "${COMP_WORDS[1]}" == 'attribute_actions_template' ]; then
43
+ true
44
+ fi
45
+ if [ "${COMP_WORDS[1]}" == 'console' ]; then
46
+ true
47
+ fi
48
+ if [ "${COMP_WORDS[1]}" == 'create_project' ]; then
49
+ if [ "${COMP_CWORD}" == "2" ]; then
50
+ COMPREPLY=()
51
+ return
52
+ fi
53
+ if [ "${COMP_CWORD}" == "3" ]; then
54
+ COMPREPLY=()
55
+ return
56
+ fi
57
+ true
58
+ fi
59
+ if [ "${COMP_WORDS[1]}" == 'generate_completion_script' ]; then
60
+ true
61
+ fi
62
+ if [ "${COMP_WORDS[1]}" == 'help' ]; then
63
+ true
64
+ fi
65
+ if [ "${COMP_WORDS[1]}" == 'model_template' ]; then
66
+ true
67
+ fi
68
+ if [ "${COMP_WORDS[1]}" == 'project_template' ]; then
69
+ true
70
+ fi
71
+ if [ "${COMP_WORDS[1]}" == 'update_attributes_from_csv' ]; then
72
+ if [ "${COMP_CWORD}" == "2" ]; then
73
+ COMPREPLY=()
74
+ return
75
+ fi
76
+ if [ "${COMP_CWORD}" == "3" ]; then
77
+ COMPREPLY=()
78
+ return
79
+ fi
80
+ if [ "${COMP_CWORD}" == "4" ]; then
81
+ COMPREPLY=()
82
+ return
83
+ fi
84
+ if [ "${COMP_CWORD}" == "5" ]; then
85
+ COMPREPLY=()
86
+ return
87
+ fi
88
+ true
89
+ fi
90
+ if [ "${COMP_WORDS[1]}" == 'validate_attribute_actions' ]; then
91
+ if [ "${COMP_CWORD}" == "2" ]; then
92
+ COMPREPLY=()
93
+ return
94
+ fi
95
+ if [ "${COMP_CWORD}" == "3" ]; then
96
+ COMPREPLY=()
97
+ return
98
+ fi
99
+ if [ "${COMP_CWORD}" == "4" ]; then
100
+ COMPREPLY=()
101
+ return
102
+ fi
103
+ true
104
+ fi
105
+ if [ "${COMP_WORDS[1]}" == 'validate_model' ]; then
106
+ if [ "${COMP_CWORD}" == "2" ]; then
107
+ COMPREPLY=()
108
+ return
109
+ fi
110
+ if [ "${COMP_CWORD}" == "3" ]; then
111
+ COMPREPLY=()
112
+ return
113
+ fi
114
+ if [ "${COMP_CWORD}" == "4" ]; then
115
+ COMPREPLY=()
116
+ return
117
+ fi
118
+ if [ "${COMP_CWORD}" == "5" ]; then
119
+ COMPREPLY=()
120
+ return
121
+ fi
122
+ true
123
+ fi
124
+ if [ "${COMP_WORDS[1]}" == 'validate_project' ]; then
125
+ if [ "${COMP_CWORD}" == "2" ]; then
126
+ COMPREPLY=()
127
+ return
128
+ fi
129
+ true
130
+ fi
131
+ fi
132
+ }
133
+ complete -o default -F _etna_app_completions etna_app
@@ -0,0 +1,20 @@
1
+ # Rubygem doesn't provide a great hook for executing completion or shared data on installing a gem by default,
2
+ # but it does provide an extension installation hook in the form of extconf.rb.
3
+ # We are 'hacking' this behavior to do a user installation of completion files on the system
4
+ require 'mkmf'
5
+
6
+ # Create a dummy extension file. Without this RubyGems would abort the
7
+ # installation process
8
+ FileUtils.touch(File.join(Dir.pwd, 'wat.' + RbConfig::CONFIG['DLEXT']))
9
+
10
+ # Drop the completion file in the user's home directory so they can wire this based on their system.
11
+ Dir.chdir(File.expand_path('..', __FILE__)) do
12
+ puts `cp ../../etna.completion ~/etna.completion`
13
+ puts "etna.completion has been copied to your home directory. Source it from your bashrc file to add completions for the etna command."
14
+ end
15
+
16
+ # This is normally set by calling create_makefile() but we don't need that
17
+ # method since we'll provide a dummy Makefile. Without setting this value
18
+ # RubyGems will abort the installation.
19
+ $makefile_created = true
20
+ create_makefile('ext')
@@ -0,0 +1,368 @@
1
+ require 'date'
2
+ require 'logger'
3
+ require 'rollbar'
4
+ require 'tempfile'
5
+ require_relative 'helpers'
6
+ require 'yaml'
7
+
8
+ # /bin/etna will confirm execution before running any command that includes this module.
9
+ module RequireConfirmation
10
+ end
11
+
12
+ class EtnaApp
13
+ def self.config_file_path
14
+ File.join(Dir.home, 'etna.yml')
15
+ end
16
+
17
+ string_flags << '--environment'
18
+
19
+ def dispatch_to_subcommand(cmd = 'help', *args, environment: nil, **kwds)
20
+ set_environment(environment)
21
+ super(cmd, *args, **kwds)
22
+ end
23
+
24
+ def environment
25
+ if @environment
26
+ @environment
27
+ elsif @config && @config.is_a?(Hash) && @config.keys.length == 1
28
+ @config.keys.last.to_sym
29
+ elsif @config && @config.is_a?(Hash) && @config.keys.length > 1
30
+ raise "You have multiple environments configured, please specify your environment by adding --environment #{@config.keys.join("|")}"
31
+ else
32
+ raise "You do not have a successfully configured environment, please run #{program_name} config set https://polyphemus.ucsf.edu"
33
+ end
34
+ end
35
+
36
+ def set_environment(env)
37
+ @environment = env.nil? ? nil : env.to_sym
38
+ end
39
+
40
+ class Config
41
+ include Etna::CommandExecutor
42
+
43
+ class Show < Etna::Command
44
+
45
+ boolean_flags << '--all'
46
+
47
+ def execute(all: false)
48
+ if all
49
+ File.open(EtnaApp.config_file_path, 'r') { |f| puts f.read }
50
+ else
51
+ puts "Current environment: #{EtnaApp.instance.environment}"
52
+ pp EtnaApp.instance.env_config
53
+ end
54
+ end
55
+ end
56
+
57
+ class Set < Etna::Command
58
+ include WithEtnaClients
59
+ include WithLogger
60
+
61
+ boolean_flags << '--ignore-ssl'
62
+
63
+ def execute(host, ignore_ssl: false)
64
+ polyphemus_client ||= Etna::Clients::Polyphemus.new(
65
+ host: host,
66
+ token: token,
67
+ ignore_ssl: ignore_ssl)
68
+ workflow = Etna::Clients::Polyphemus::SetConfigurationWorkflow.new(
69
+ polyphemus_client: polyphemus_client,
70
+ config_file: EtnaApp.config_file_path)
71
+ config = workflow.update_configuration_file(ignore_ssl: ignore_ssl)
72
+ logger.info("Updated #{config.environment} configuration from #{host}.")
73
+ end
74
+
75
+ def setup(config)
76
+ super
77
+ EtnaApp.instance.setup_logger
78
+ end
79
+ end
80
+ end
81
+
82
+ class CreateTemplate
83
+ include Etna::CommandExecutor
84
+
85
+ # TODO: Refactor and replace with a command workflow similar to AddProjectModels
86
+ class AttributeActions < Etna::Command
87
+ def execute
88
+ spec = Gem::Specification.find_by_name("etna")
89
+ gem_root = spec.gem_dir
90
+ FileUtils.cp(
91
+ "#{gem_root}/lib/etna/templates/attribute_actions_template.json",
92
+ 'attribute_actions_template.json')
93
+ puts "A sample attribute actions JSON template has been provided in the current directory as `attribute_actions_template.json`."
94
+ end
95
+ end
96
+ end
97
+
98
+ class Administrate
99
+ include Etna::CommandExecutor
100
+
101
+ class Project
102
+ include Etna::CommandExecutor
103
+
104
+ class Create < Etna::Command
105
+ include WithEtnaClients
106
+ include WithLogger
107
+
108
+ def execute(project_name, project_name_full)
109
+ create_args = {project_name: project_name, project_name_full: project_name_full}
110
+ Etna::Clients::Magma::ProjectValidator.new(**create_args).validate!("#{program_name} args invalid!")
111
+
112
+ create_project_workflow = Etna::Clients::Magma::CreateProjectWorkflow.new(
113
+ magma_client: magma_client,
114
+ janus_client: janus_client,
115
+ **create_args
116
+ )
117
+ create_project_workflow.create!
118
+ end
119
+ end
120
+ end
121
+
122
+ class Models
123
+ include Etna::CommandExecutor
124
+
125
+ class CopyTemplate < Etna::Command
126
+ include WithEtnaClients
127
+ include WithLogger
128
+ include StrongConfirmation
129
+
130
+ string_flags << '--file'
131
+ string_flags << '--target-model'
132
+
133
+ def execute(project_name, target_model: 'project', file: "#{project_name}_models_#{target_model}_tree.csv")
134
+ unless File.exists?(file)
135
+ puts "File #{file} is being prepared from the #{project_name} project."
136
+ puts "Copying models descending from #{target_model}..."
137
+ prepare_template(file, project_name, target_model)
138
+ puts
139
+ puts "Done! You can start editing the file #{file} now"
140
+ else
141
+ puts "File #{file} already exists! Please remove or specify a different file name before running again."
142
+ end
143
+ end
144
+
145
+ def workflow
146
+ @workflow ||= Etna::Clients::Magma::AddProjectModelsWorkflow.new(magma_client: magma_client)
147
+ end
148
+
149
+ def prepare_template(file, project_name, target_model)
150
+ tf = Tempfile.new
151
+
152
+ begin
153
+ File.open(tf.path, 'wb') { |f| workflow.write_models_template_csv(f, project_name, target_model) }
154
+ FileUtils.cp(tf.path, file)
155
+ ensure
156
+ tf.close!
157
+ end
158
+ end
159
+ end
160
+
161
+ class ApplyTemplate < Etna::Command
162
+ include WithEtnaClients
163
+ include StrongConfirmation
164
+ include WithLogger
165
+
166
+ string_flags << '--file'
167
+ string_flags << '--target-model'
168
+
169
+ def execute(project_name, target_model: 'project', file: "#{project_name}_models_#{target_model}_tree.csv")
170
+ reset
171
+
172
+ unless File.exists?(file)
173
+ puts "Could not find file #{file}"
174
+ return
175
+ end
176
+
177
+ load_models_from_csv(file)
178
+
179
+ while true
180
+ if @changeset && @errors.empty?
181
+ puts "File #{file} is well formatted. Calculating expected changes..."
182
+ sync_workflow = workflow.plan_synchronization(@changeset, project_name, target_model)
183
+ models_and_action_types = sync_workflow.planned_actions.map { |a| Etna::Clients::Magma::ModelSynchronizationWorkflow.models_affected_by(a).map { |m| [m, a.action_name] }.flatten }
184
+ models_and_action_types.sort!
185
+ models_and_action_types = models_and_action_types.group_by(&:first)
186
+
187
+ models_and_action_types.each do |model, actions|
188
+ actions = actions.map { |a| a[1] }.sort
189
+ actions = actions.group_by { |v| v }
190
+ puts
191
+ puts "#{model} changes:"
192
+ actions.each do |type, actions|
193
+ puts " * #{type}: #{actions.length}"
194
+ end
195
+ end
196
+
197
+ puts
198
+ puts "Would you like to execute?"
199
+ if confirm
200
+ sync_workflow.update_block = Proc.new do |action|
201
+ puts "Executing #{action.action_name} on #{Etna::Clients::Magma::ModelSynchronizationWorkflow.models_affected_by(action)}..."
202
+ end
203
+
204
+ sync_workflow.execute_planned!
205
+ File.unlink(file)
206
+ end
207
+
208
+ return
209
+ end
210
+
211
+ # Poll for updates
212
+ puts "Watching for changes to #{file}..."
213
+ while File.stat(file).mtime == @last_load
214
+ sleep(1)
215
+ end
216
+
217
+ load_models_from_csv(file)
218
+ end
219
+ end
220
+
221
+ def workflow
222
+ @workflow ||= Etna::Clients::Magma::AddProjectModelsWorkflow.new(magma_client: magma_client)
223
+ end
224
+
225
+ def reset
226
+ @errors = []
227
+ @changeset = nil
228
+ @last_load = Time.at(0)
229
+ end
230
+
231
+ def load_models_from_csv(file)
232
+ reset
233
+
234
+ @last_load = File.stat(file).mtime
235
+ @changeset = File.open(file, 'r') do |f|
236
+ workflow.prepare_changeset_from_csv(f) do |err|
237
+ @errors << err
238
+ end
239
+ end
240
+
241
+ return if @errors.empty?
242
+
243
+ puts "Input file #{file} is invalid:"
244
+ @errors.each do |err|
245
+ puts " * " + err.gsub("\n", "\n\t")
246
+ end
247
+ end
248
+ end
249
+
250
+ class Attributes
251
+ include Etna::CommandExecutor
252
+
253
+ class UpdateFromCsv < Etna::Command
254
+ include WithEtnaClients
255
+ include WithLogger
256
+ include RequireConfirmation
257
+
258
+ def magma_crud
259
+ @magma_crud ||= Etna::Clients::Magma::MagmaCrudWorkflow.new(
260
+ magma_client: magma_client,
261
+ project_name: @project_name)
262
+ end
263
+
264
+ def execute(project_name, model_name, filepath)
265
+ @project_name = project_name
266
+
267
+ update_attributes_workflow = Etna::Clients::Magma::UpdateAttributesFromCsvWorkflowSingleModel.new(
268
+ magma_crud: magma_crud,
269
+ project_name: project_name,
270
+ model_name: model_name,
271
+ filepath: filepath)
272
+ update_attributes_workflow.update_attributes
273
+ end
274
+ end
275
+ end
276
+ end
277
+ end
278
+
279
+ class Polyphemus < Etna::Command
280
+ include WithEtnaClients
281
+ string_flags << '--tag'
282
+ boolean_flags << '--local'
283
+
284
+ def execute(tag: nil, local: false)
285
+ @tag = tag || default_tag
286
+ @local = local
287
+ create_config_yml
288
+
289
+ unless @local
290
+ puts `docker pull #{image}`
291
+ end
292
+
293
+ puts run_cmd
294
+ exec(run_cmd)
295
+ end
296
+
297
+ def run_cmd
298
+ parts = ['docker run --rm -it']
299
+
300
+ unless Gem.win_platform?
301
+ uid = Etc.getpwnam(ENV['USER']).uid
302
+ gid = Etc.getgrnam('docker').gid
303
+ parts << "-u #{uid}:#{gid}"
304
+ parts << "-v /etc/passwd:/etc/passwd:ro"
305
+ parts << "-v /etc/group:/etc/group:ro"
306
+ end
307
+
308
+ parts << "-v #{temp_config_file.path}:/app/config.yml:ro"
309
+ parts << "-v #{Dir.pwd}:/app/workspace"
310
+ parts << "-e HOME=/root"
311
+
312
+ parts << image
313
+ parts << "bash -c 'cd /app/workspace && exec bash --rcfile /root/.bashrc'"
314
+
315
+ parts.join(' ')
316
+ end
317
+
318
+ def temp_config_file
319
+ @temp_config_file ||= Tempfile.new
320
+ end
321
+
322
+ def create_config_yml
323
+ host_config = [:metis, :magma, :janus, :timur].map do |host|
324
+ [host, (EtnaApp.instance.config(host) || {}).update(token: token)]
325
+ end.to_h
326
+
327
+ config = {
328
+ EtnaApp.instance.environment => {
329
+ log_file: '/dev/stdout',
330
+ log_level: 'info',
331
+ }.update(host_config)
332
+ }
333
+
334
+ File.open(temp_config_file.path, 'w') { |f| YAML.dump(config, f) }
335
+ end
336
+
337
+ def default_tag
338
+ docker = EtnaApp.instance.config(:docker)
339
+ if docker.nil?
340
+ nil
341
+ else
342
+ docker[:default_tag]
343
+ end || 'master'
344
+ end
345
+
346
+ def image
347
+ if @local
348
+ "polyphemus:#{@tag}"
349
+ else
350
+ "etnaagent/polyphemus:#{@tag}"
351
+ end
352
+ end
353
+ end
354
+
355
+ class Console < Etna::Command
356
+ usage 'Open a console with a connected Etna instance.'
357
+
358
+ def execute
359
+ require 'irb'
360
+ ARGV.clear
361
+ IRB.start
362
+ end
363
+
364
+ def setup(config)
365
+ super
366
+ end
367
+ end
368
+ end