etna 0.1.14 → 0.1.20

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 +18 -0
  3. data/etna.completion +1001 -0
  4. data/etna_app.completion +133 -0
  5. data/ext/completions/extconf.rb +20 -0
  6. data/lib/commands.rb +395 -0
  7. data/lib/etna.rb +7 -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 +354 -0
  19. data/lib/etna/clients/magma/models.rb +630 -0
  20. data/lib/etna/clients/magma/workflows.rb +10 -0
  21. data/lib/etna/clients/magma/workflows/add_project_models_workflow.rb +67 -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 +123 -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 +452 -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 +246 -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/csvs.rb +159 -0
  47. data/lib/etna/directed_graph.rb +56 -0
  48. data/lib/etna/environment_scoped.rb +19 -0
  49. data/lib/etna/errors.rb +6 -0
  50. data/lib/etna/generate_autocompletion_script.rb +131 -0
  51. data/lib/etna/json_serializable_struct.rb +37 -0
  52. data/lib/etna/logger.rb +24 -2
  53. data/lib/etna/multipart_serializable_nested_hash.rb +50 -0
  54. data/lib/etna/route.rb +1 -1
  55. data/lib/etna/server.rb +3 -0
  56. data/lib/etna/spec/vcr.rb +99 -0
  57. data/lib/etna/templates/attribute_actions_template.json +43 -0
  58. data/lib/etna/test_auth.rb +3 -1
  59. data/lib/etna/user.rb +11 -1
  60. data/lib/helpers.rb +90 -0
  61. metadata +70 -5
@@ -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,395 @@
1
+ require 'date'
2
+ require 'logger'
3
+ require 'rollbar'
4
+ require 'tempfile'
5
+ require_relative 'helpers'
6
+ require 'yaml'
7
+
8
+ class EtnaApp
9
+ def self.config_file_path
10
+ File.join(Dir.home, 'etna.yml')
11
+ end
12
+
13
+ string_flags << '--environment'
14
+
15
+ def dispatch_to_subcommand(cmd = 'help', *args, environment: nil, **kwds)
16
+ set_environment(environment)
17
+ super(cmd, *args, **kwds)
18
+ end
19
+
20
+ def environment
21
+ if @environment
22
+ @environment
23
+ elsif @config && @config.is_a?(Hash) && @config.keys.length == 1
24
+ @config.keys.last.to_sym
25
+ elsif @config && @config.is_a?(Hash) && @config.keys.length > 1
26
+ :many
27
+ else
28
+ :none
29
+ end
30
+ end
31
+
32
+ def set_environment(env)
33
+ @environment = env.nil? ? nil : env.to_sym
34
+ end
35
+
36
+ class Config
37
+ include Etna::CommandExecutor
38
+
39
+ class Show < Etna::Command
40
+ def execute
41
+ if EtnaApp.instance.environment == :many
42
+ File.open(EtnaApp.config_file_path, 'r') { |f| puts f.read }
43
+ else
44
+ puts "Current environment: #{EtnaApp.instance.environment}"
45
+ pp EtnaApp.instance.env_config
46
+ end
47
+ end
48
+ end
49
+
50
+ class Set < Etna::Command
51
+ include WithEtnaClients
52
+ include WithLogger
53
+
54
+ boolean_flags << '--ignore-ssl'
55
+
56
+ def execute(host, ignore_ssl: false)
57
+ polyphemus_client ||= Etna::Clients::Polyphemus.new(
58
+ host: host,
59
+ token: token(ignore_environment: true),
60
+ ignore_ssl: ignore_ssl)
61
+ workflow = Etna::Clients::Polyphemus::SetConfigurationWorkflow.new(
62
+ polyphemus_client: polyphemus_client,
63
+ config_file: EtnaApp.config_file_path)
64
+ config = workflow.update_configuration_file(ignore_ssl: ignore_ssl)
65
+ logger.info("Updated #{config.environment} configuration from #{host}.")
66
+ end
67
+
68
+ def setup(config)
69
+ super
70
+ EtnaApp.instance.setup_logger
71
+ end
72
+ end
73
+ end
74
+
75
+ class CreateTemplate
76
+ include Etna::CommandExecutor
77
+
78
+ # TODO: Refactor and replace with a command workflow similar to AddProjectModels
79
+ class AttributeActions < Etna::Command
80
+ def execute
81
+ spec = Gem::Specification.find_by_name("etna")
82
+ gem_root = spec.gem_dir
83
+ FileUtils.cp(
84
+ "#{gem_root}/lib/etna/templates/attribute_actions_template.json",
85
+ 'attribute_actions_template.json')
86
+ puts "A sample attribute actions JSON template has been provided in the current directory as `attribute_actions_template.json`."
87
+ end
88
+ end
89
+ end
90
+
91
+ class Administrate
92
+ include Etna::CommandExecutor
93
+
94
+ class Project
95
+ include Etna::CommandExecutor
96
+
97
+ class Create < Etna::Command
98
+ include WithEtnaClients
99
+ include WithLogger
100
+
101
+ def execute(project_name, project_name_full)
102
+ create_args = {project_name: project_name, project_name_full: project_name_full}
103
+ Etna::Clients::Magma::ProjectValidator.new(**create_args).validate!("#{program_name} args invalid!")
104
+
105
+ create_project_workflow = Etna::Clients::Magma::CreateProjectWorkflow.new(
106
+ magma_client: magma_client,
107
+ janus_client: janus_client,
108
+ **create_args
109
+ )
110
+ create_project_workflow.create!
111
+ end
112
+ end
113
+ end
114
+
115
+ class Models
116
+ include Etna::CommandExecutor
117
+
118
+ class CopyTemplate < Etna::Command
119
+ include WithEtnaClients
120
+ include WithLogger
121
+ include StrongConfirmation
122
+
123
+ string_flags << '--file'
124
+ string_flags << '--target-model'
125
+
126
+ def execute(project_name, target_model: 'project', file: "#{project_name}_models_#{target_model}_tree.csv")
127
+ unless File.exists?(file)
128
+ puts "File #{file} is being prepared from the #{project_name} project."
129
+ puts "Copying models descending from #{target_model}..."
130
+ prepare_template(file, project_name, target_model)
131
+ puts
132
+ puts "Done! You can start editing the file #{file} now"
133
+ else
134
+ puts "File #{file} already exists! Please remove or specify a different file name before running again."
135
+ end
136
+ end
137
+
138
+ def workflow
139
+ @workflow ||= Etna::Clients::Magma::AddProjectModelsWorkflow.new(magma_client: magma_client)
140
+ end
141
+
142
+ def prepare_template(file, project_name, target_model)
143
+ tf = Tempfile.new
144
+
145
+ begin
146
+ File.open(tf.path, 'wb') { |f| workflow.write_models_template_csv(project_name, target_model, io: f) }
147
+ FileUtils.cp(tf.path, file)
148
+ ensure
149
+ tf.close!
150
+ end
151
+ end
152
+ end
153
+
154
+ class ApplyTemplate < Etna::Command
155
+ include WithEtnaClients
156
+ include StrongConfirmation
157
+ include WithLogger
158
+
159
+ string_flags << '--file'
160
+ string_flags << '--target-model'
161
+
162
+ def execute(project_name, target_model: 'project', file: "#{project_name}_models_#{target_model}_tree.csv")
163
+ reset
164
+
165
+ unless File.exists?(file)
166
+ puts "Could not find file #{file}"
167
+ return
168
+ end
169
+
170
+ load_models_from_csv(file)
171
+
172
+ while true
173
+ if @changeset && @errors.empty?
174
+ puts "File #{file} is well formatted. Calculating expected changes..."
175
+ sync_workflow = workflow.plan_synchronization(@changeset, project_name, target_model)
176
+ 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 }
177
+ models_and_action_types.sort!
178
+ models_and_action_types = models_and_action_types.group_by(&:first)
179
+
180
+ models_and_action_types.each do |model, actions|
181
+ actions = actions.map { |a| a[1] }.sort
182
+ actions = actions.group_by { |v| v }
183
+ puts
184
+ puts "#{model} changes:"
185
+ actions.each do |type, actions|
186
+ puts " * #{type}: #{actions.length}"
187
+ end
188
+ end
189
+
190
+ puts
191
+ puts "Would you like to execute?"
192
+ if confirm
193
+ sync_workflow.update_block = Proc.new do |action|
194
+ puts "Executing #{action.action_name} on #{Etna::Clients::Magma::ModelSynchronizationWorkflow.models_affected_by(action)}..."
195
+ end
196
+
197
+ sync_workflow.execute_planned!
198
+ File.unlink(file)
199
+ end
200
+
201
+ return
202
+ end
203
+
204
+ # Poll for updates
205
+ puts "Watching for changes to #{file}..."
206
+ while File.stat(file).mtime == @last_load
207
+ sleep(1)
208
+ end
209
+
210
+ load_models_from_csv(file)
211
+ end
212
+ end
213
+
214
+ def workflow
215
+ @workflow ||= Etna::Clients::Magma::AddProjectModelsWorkflow.new(magma_client: magma_client)
216
+ end
217
+
218
+ def reset
219
+ @errors = []
220
+ @changeset = nil
221
+ @last_load = Time.at(0)
222
+ end
223
+
224
+ def load_models_from_csv(file)
225
+ reset
226
+
227
+ @last_load = File.stat(file).mtime
228
+ @changeset = File.open(file, 'r') do |f|
229
+ workflow.prepare_changeset_from_csv(f) do |err|
230
+ @errors << err
231
+ end
232
+ end
233
+
234
+ return if @errors.empty?
235
+
236
+ puts "Input file #{file} is invalid:"
237
+ @errors.each do |err|
238
+ puts " * " + err.gsub("\n", "\n\t")
239
+ end
240
+ end
241
+ end
242
+
243
+ class Attributes
244
+ include Etna::CommandExecutor
245
+
246
+ class UpdateFromCsv < Etna::Command
247
+ include WithEtnaClients
248
+ include WithLogger
249
+
250
+ boolean_flags << '--json-values'
251
+ string_flags << '--hole-value'
252
+
253
+ def magma_crud
254
+ @magma_crud ||= Etna::Clients::Magma::MagmaCrudWorkflow.new(
255
+ magma_client: magma_client,
256
+ project_name: @project_name)
257
+ end
258
+
259
+ def execute(project_name, model_name, filepath, hole_value: '_', json_values: false)
260
+ @project_name = project_name
261
+
262
+ update_attributes_workflow = Etna::Clients::Magma::UpdateAttributesFromCsvWorkflowSingleModel.new(
263
+ magma_crud: magma_crud,
264
+ project_name: project_name,
265
+ model_name: model_name,
266
+ filepath: filepath,
267
+ hole_value: hole_value,
268
+ json_values: json_values)
269
+ update_attributes_workflow.update_attributes
270
+ end
271
+ end
272
+
273
+ class CreateFileLinkingCsv < Etna::Command
274
+ include WithEtnaClients
275
+
276
+ string_flags << '--file'
277
+ string_flags << '--regex'
278
+ string_flags << '--folder'
279
+ boolean_flags << '--collection'
280
+
281
+ def execute(project_name, bucket_name, attribute_name, extension, collection: false, regex: "**/*/(?<identifier>.+)\\.#{extension}$", folder: "", file: "#{project_name}_#{attribute_name}.csv")
282
+ if folder.start_with?("/")
283
+ folder = folder.slice(1..-1)
284
+ end
285
+
286
+ regex = Regexp.new(regex)
287
+
288
+ workflow = Etna::Clients::Magma::SimpleFileLinkingWorkflow.new(
289
+ metis_client: metis_client,
290
+ project_name: project_name,
291
+ bucket_name: bucket_name,
292
+ folder: folder,
293
+ extension: extension,
294
+ attribute_name: attribute_name,
295
+ regex: regex,
296
+ file_collection: collection,
297
+ )
298
+
299
+ workflow.write_csv_io(filename: file)
300
+ end
301
+ end
302
+ end
303
+ end
304
+ end
305
+
306
+ class Polyphemus < Etna::Command
307
+ include WithEtnaClients
308
+ string_flags << '--tag'
309
+ boolean_flags << '--local'
310
+
311
+ def execute(tag: nil, local: false)
312
+ @tag = tag || default_tag
313
+ @local = local
314
+ create_config_yml
315
+
316
+ unless @local
317
+ puts `docker pull #{image}`
318
+ end
319
+
320
+ puts run_cmd
321
+ exec(run_cmd)
322
+ end
323
+
324
+ def run_cmd
325
+ parts = ['docker run --rm -it']
326
+
327
+ unless Gem.win_platform?
328
+ uid = Etc.getpwnam(ENV['USER']).uid
329
+ gid = Etc.getgrnam('docker').gid
330
+ parts << "-u #{uid}:#{gid}"
331
+ parts << "-v /etc/passwd:/etc/passwd:ro"
332
+ parts << "-v /etc/group:/etc/group:ro"
333
+ end
334
+
335
+ parts << "-v #{temp_config_file.path}:/app/config.yml:ro"
336
+ parts << "-v #{Dir.pwd}:/app/workspace"
337
+ parts << "-e HOME=/root"
338
+
339
+ parts << image
340
+ parts << "bash -c 'cd /app/workspace && exec bash --rcfile /root/.bashrc'"
341
+
342
+ parts.join(' ')
343
+ end
344
+
345
+ def temp_config_file
346
+ @temp_config_file ||= Tempfile.new
347
+ end
348
+
349
+ def create_config_yml
350
+ host_config = [:metis, :magma, :janus, :timur].map do |host|
351
+ [host, (EtnaApp.instance.config(host) || {}).update(token: token)]
352
+ end.to_h
353
+
354
+ config = {
355
+ EtnaApp.instance.environment => {
356
+ log_file: '/dev/stdout',
357
+ log_level: 'info',
358
+ }.update(host_config)
359
+ }
360
+
361
+ File.open(temp_config_file.path, 'w') { |f| YAML.dump(config, f) }
362
+ end
363
+
364
+ def default_tag
365
+ docker = EtnaApp.instance.config(:docker)
366
+ if docker.nil?
367
+ nil
368
+ else
369
+ docker[:default_tag]
370
+ end || 'master'
371
+ end
372
+
373
+ def image
374
+ if @local
375
+ "polyphemus:#{@tag}"
376
+ else
377
+ "etnaagent/polyphemus:#{@tag}"
378
+ end
379
+ end
380
+ end
381
+
382
+ class Console < Etna::Command
383
+ usage 'Open a console with a connected Etna instance.'
384
+
385
+ def execute
386
+ require 'irb'
387
+ ARGV.clear
388
+ IRB.start
389
+ end
390
+
391
+ def setup(config)
392
+ super
393
+ end
394
+ end
395
+ end