simplygenius-atmos 0.7.1 → 0.8.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (87) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +4 -4
  3. data/exe/atmos +2 -2
  4. data/lib/{atmos.rb → simplygenius/atmos.rb} +9 -7
  5. data/lib/simplygenius/atmos/cli.rb +116 -0
  6. data/lib/simplygenius/atmos/commands/account.rb +69 -0
  7. data/lib/simplygenius/atmos/commands/apply.rb +24 -0
  8. data/lib/simplygenius/atmos/commands/auth_exec.rb +34 -0
  9. data/lib/simplygenius/atmos/commands/base_command.rb +16 -0
  10. data/lib/simplygenius/atmos/commands/bootstrap.rb +76 -0
  11. data/lib/simplygenius/atmos/commands/container.rb +62 -0
  12. data/lib/simplygenius/atmos/commands/destroy.rb +22 -0
  13. data/lib/simplygenius/atmos/commands/generate.rb +187 -0
  14. data/lib/simplygenius/atmos/commands/init.rb +22 -0
  15. data/lib/simplygenius/atmos/commands/new.rb +22 -0
  16. data/lib/simplygenius/atmos/commands/otp.rb +58 -0
  17. data/lib/simplygenius/atmos/commands/plan.rb +24 -0
  18. data/lib/simplygenius/atmos/commands/secret.rb +91 -0
  19. data/lib/simplygenius/atmos/commands/terraform.rb +56 -0
  20. data/lib/simplygenius/atmos/commands/user.rb +78 -0
  21. data/lib/simplygenius/atmos/config.rb +279 -0
  22. data/lib/simplygenius/atmos/exceptions.rb +13 -0
  23. data/lib/simplygenius/atmos/generator.rb +232 -0
  24. data/lib/simplygenius/atmos/ipc.rb +136 -0
  25. data/lib/simplygenius/atmos/ipc_actions/notify.rb +31 -0
  26. data/lib/simplygenius/atmos/ipc_actions/ping.rb +23 -0
  27. data/lib/simplygenius/atmos/logging.rb +164 -0
  28. data/lib/simplygenius/atmos/otp.rb +62 -0
  29. data/lib/simplygenius/atmos/plugin.rb +27 -0
  30. data/lib/simplygenius/atmos/plugin_manager.rb +120 -0
  31. data/lib/simplygenius/atmos/plugins/output_filter.rb +29 -0
  32. data/lib/simplygenius/atmos/plugins/prompt_notify.rb +21 -0
  33. data/lib/simplygenius/atmos/provider_factory.rb +23 -0
  34. data/lib/simplygenius/atmos/providers/aws/account_manager.rb +83 -0
  35. data/lib/simplygenius/atmos/providers/aws/auth_manager.rb +220 -0
  36. data/lib/simplygenius/atmos/providers/aws/container_manager.rb +118 -0
  37. data/lib/simplygenius/atmos/providers/aws/provider.rb +53 -0
  38. data/lib/simplygenius/atmos/providers/aws/s3_secret_manager.rb +51 -0
  39. data/lib/simplygenius/atmos/providers/aws/user_manager.rb +213 -0
  40. data/lib/simplygenius/atmos/settings_hash.rb +93 -0
  41. data/lib/simplygenius/atmos/source_path.rb +186 -0
  42. data/lib/simplygenius/atmos/template.rb +117 -0
  43. data/lib/simplygenius/atmos/terraform_executor.rb +297 -0
  44. data/lib/simplygenius/atmos/ui.rb +173 -0
  45. data/lib/simplygenius/atmos/utils.rb +54 -0
  46. data/lib/simplygenius/atmos/version.rb +5 -0
  47. data/templates/new/config/atmos.yml +21 -13
  48. data/templates/new/config/atmos/recipes.yml +16 -0
  49. data/templates/new/config/atmos/runtime.yml +9 -0
  50. metadata +46 -40
  51. data/lib/atmos/cli.rb +0 -105
  52. data/lib/atmos/commands/account.rb +0 -65
  53. data/lib/atmos/commands/apply.rb +0 -20
  54. data/lib/atmos/commands/auth_exec.rb +0 -29
  55. data/lib/atmos/commands/base_command.rb +0 -12
  56. data/lib/atmos/commands/bootstrap.rb +0 -72
  57. data/lib/atmos/commands/container.rb +0 -58
  58. data/lib/atmos/commands/destroy.rb +0 -18
  59. data/lib/atmos/commands/generate.rb +0 -90
  60. data/lib/atmos/commands/init.rb +0 -18
  61. data/lib/atmos/commands/new.rb +0 -18
  62. data/lib/atmos/commands/otp.rb +0 -54
  63. data/lib/atmos/commands/plan.rb +0 -20
  64. data/lib/atmos/commands/secret.rb +0 -87
  65. data/lib/atmos/commands/terraform.rb +0 -52
  66. data/lib/atmos/commands/user.rb +0 -74
  67. data/lib/atmos/config.rb +0 -208
  68. data/lib/atmos/exceptions.rb +0 -9
  69. data/lib/atmos/generator.rb +0 -199
  70. data/lib/atmos/generator_factory.rb +0 -93
  71. data/lib/atmos/ipc.rb +0 -132
  72. data/lib/atmos/ipc_actions/notify.rb +0 -27
  73. data/lib/atmos/ipc_actions/ping.rb +0 -19
  74. data/lib/atmos/logging.rb +0 -160
  75. data/lib/atmos/otp.rb +0 -61
  76. data/lib/atmos/provider_factory.rb +0 -19
  77. data/lib/atmos/providers/aws/account_manager.rb +0 -82
  78. data/lib/atmos/providers/aws/auth_manager.rb +0 -208
  79. data/lib/atmos/providers/aws/container_manager.rb +0 -116
  80. data/lib/atmos/providers/aws/provider.rb +0 -51
  81. data/lib/atmos/providers/aws/s3_secret_manager.rb +0 -49
  82. data/lib/atmos/providers/aws/user_manager.rb +0 -211
  83. data/lib/atmos/settings_hash.rb +0 -90
  84. data/lib/atmos/terraform_executor.rb +0 -267
  85. data/lib/atmos/ui.rb +0 -159
  86. data/lib/atmos/utils.rb +0 -50
  87. data/lib/atmos/version.rb +0 -3
@@ -1,267 +0,0 @@
1
- require_relative '../atmos'
2
- require_relative '../atmos/ipc'
3
- require_relative '../atmos/ui'
4
- require 'open3'
5
- require 'fileutils'
6
- require 'find'
7
- require 'climate_control'
8
-
9
- module Atmos
10
-
11
- class TerraformExecutor
12
- include GemLogger::LoggerSupport
13
- include FileUtils
14
- include Atmos::UI
15
-
16
- class ProcessFailed < RuntimeError; end
17
-
18
- def initialize(process_env: ENV, working_group: 'default')
19
- @process_env = process_env
20
- @working_group = working_group
21
- @working_dir = Atmos.config.tf_working_dir(@working_group)
22
- @recipes = Atmos.config["recipes.#{@working_group}"]
23
- end
24
-
25
- def run(*terraform_args, skip_backend: false, skip_secrets: false, get_modules: false, output_io: nil)
26
- setup_working_dir(skip_backend: skip_backend)
27
-
28
- if get_modules
29
- logger.debug("Getting modules")
30
- get_modules_io = StringIO.new
31
- begin
32
- execute("get", output_io: get_modules_io)
33
- rescue Atmos::TerraformExecutor::ProcessFailed => e
34
- logger.info(get_modules_io.string)
35
- raise
36
- end
37
- end
38
-
39
- return execute(*terraform_args, skip_secrets: skip_secrets, output_io: output_io)
40
- end
41
-
42
- private
43
-
44
- def tf_cmd(*args)
45
- ['terraform'] + args
46
- end
47
-
48
- def execute(*terraform_args, skip_secrets: false, output_io: nil)
49
- cmd = tf_cmd(*terraform_args)
50
- logger.debug("Running terraform: #{cmd.join(' ')}")
51
-
52
- env = Hash[@process_env]
53
- if ! skip_secrets
54
- begin
55
- env = env.merge(secrets_env)
56
- rescue => e
57
- logger.debug("Secrets not available: #{e}")
58
- end
59
- end
60
-
61
- # lets tempfiles create by subprocesses be easily found by users
62
- env['TMPDIR'] = Atmos.config.tmp_dir
63
-
64
- # Lets terraform communicate back to atmos, e.g. for UI notifications
65
- ipc = Atmos::Ipc.new(Atmos.config.tmp_dir)
66
-
67
- IO.pipe do |stdout, stdout_writer|
68
- IO.pipe do |stderr, stderr_writer|
69
-
70
- stdout_writer.sync = stderr_writer.sync = true
71
- # TODO: more filtering on terraform output?
72
- stdout_thr = pipe_stream(stdout, output_io.nil? ? $stdout : output_io) do |data|
73
- if data =~ /^[\e\[\dm\s]*Enter a value:[\e\[\dm\s]*$/
74
- notify(message: "Terraform is waiting for user input")
75
- end
76
- data
77
- end
78
- stderr_thr = pipe_stream(stderr, output_io.nil? ? $stderr : output_io)
79
-
80
- ipc.listen do |sock_path|
81
-
82
- if Atmos.config['ipc.disable']
83
- # Using : as the command makes execution of ipc from the
84
- # terraform side a no-op in both cases of how we call it. This
85
- # way, terraform execution continues to work when IPC is disabled
86
- # command = "$ATMOS_IPC_CLIENT <json_string>"
87
- # program = ["sh", "-c", "$ATMOS_IPC_CLIENT"]
88
- env['ATMOS_IPC_CLIENT'] = ":"
89
- else
90
- env['ATMOS_IPC_SOCK'] = sock_path
91
- env['ATMOS_IPC_CLIENT'] = ipc.generate_client_script
92
- end
93
-
94
- # Was unable to get piping to work with stdin for some reason. It
95
- # worked in simple case, but started to fail when terraform config
96
- # got more extensive. Thus, using spawn to redirect stdin from the
97
- # terminal direct to terraform, with IO.pipe to copy the outher
98
- # streams. Maybe in the future we can completely disconnect stdin
99
- # and have atmos do the output parsing and stdin prompting
100
- pid = spawn(env, *cmd,
101
- chdir: tf_recipes_dir,
102
- :out=>stdout_writer, :err=> stderr_writer, :in => :in)
103
-
104
- logger.debug("Terraform started with pid #{pid}")
105
- Process.wait(pid)
106
- end
107
-
108
- stdout_writer.close
109
- stderr_writer.close
110
- stdout_thr.join
111
- stderr_thr.join
112
-
113
- status = $?.exitstatus
114
- logger.debug("Terraform exited: #{status}")
115
- if status != 0
116
- raise ProcessFailed.new "Terraform exited with non-zero exit code: #{status}"
117
- end
118
-
119
- end
120
- end
121
-
122
- end
123
-
124
- def setup_working_dir(skip_backend: false)
125
- clean_links
126
- link_shared_plugin_dir
127
- link_support_dirs
128
- link_recipes
129
- write_atmos_vars
130
- setup_backend(skip_backend)
131
- end
132
-
133
- def setup_backend(skip_backend=false)
134
- backend_file = File.join(tf_recipes_dir, 'atmos-backend.tf.json')
135
- backend_config = (Atmos.config["backend"] || {}).clone
136
-
137
- if backend_config.present? && ! skip_backend
138
- logger.debug("Writing out terraform state backend config")
139
-
140
- # Use a different state file per group
141
- if @working_group
142
- backend_config['key'] = "#{@working_group}-#{backend_config['key']}"
143
- end
144
-
145
- backend_type = backend_config.delete("type")
146
-
147
- backend = {
148
- "terraform" => {
149
- "backend" => {
150
- backend_type => backend_config
151
- }
152
- }
153
- }
154
-
155
- File.write(backend_file, JSON.pretty_generate(backend))
156
- else
157
- logger.debug("Clearing terraform state backend config")
158
- File.delete(backend_file) if File.exist?(backend_file)
159
- end
160
- end
161
-
162
- # terraform currently (v0.11.3) doesn't handle maps with nested maps or
163
- # lists well, so flatten them - nested maps get expanded into the top level
164
- # one, with their keys being appended with underscores, and lists get
165
- # joined with "," so we end up with a single hash with homogenous types
166
- def homogenize_for_terraform(h, root={}, prefix="")
167
- h.each do |k, v|
168
- if v.is_a? Hash
169
- homogenize_for_terraform(v, root, "#{k}_")
170
- else
171
- v = v.join(",") if v.is_a? Array
172
- root["#{prefix}#{k}"] = v
173
- end
174
- end
175
- return root
176
- end
177
-
178
- def tf_recipes_dir
179
- @tf_recipes_dir ||= begin
180
- dir = File.join(@working_dir, 'recipes')
181
- logger.debug("Tf recipes dir: #{dir}")
182
- mkdir_p(dir)
183
- dir
184
- end
185
- end
186
-
187
- def write_atmos_vars
188
- File.open(File.join(tf_recipes_dir, 'atmos.auto.tfvars.json'), 'w') do |f|
189
- atmos_var_config = atmos_config = homogenize_for_terraform(Atmos.config.to_h)
190
-
191
- var_prefix = Atmos.config['var_prefix']
192
- if var_prefix
193
- atmos_var_config = Hash[atmos_var_config.collect {|k, v| ["#{var_prefix}#{k}", v]}]
194
- end
195
-
196
- var_hash = {
197
- atmos_env: Atmos.config.atmos_env,
198
- all_env_names: Atmos.config.all_env_names,
199
- account_ids: Atmos.config.account_hash,
200
- atmos_config: atmos_config
201
- }
202
- var_hash = var_hash.merge(atmos_var_config)
203
- f.puts(JSON.pretty_generate(var_hash))
204
- end
205
- end
206
-
207
- def secrets_env
208
- # NOTE use an auto-deleting temp file if passing secrets through env ends
209
- # up being problematic
210
- # TODO fix the need for CC - TE calls for secrets which needs auth in
211
- # ENV, so kinda clunk to have to do both CC and pass the env in
212
- ClimateControl.modify(@process_env) do
213
- secrets = Atmos.config.provider.secret_manager.to_h
214
- env_secrets = Hash[secrets.collect { |k, v| ["TF_VAR_#{k}", v] }]
215
- return env_secrets
216
- end
217
- end
218
-
219
- def clean_links
220
- Find.find(@working_dir) do |f|
221
- Find.prune if f =~ /\/.terraform\/modules\//
222
- File.delete(f) if File.symlink?(f)
223
- end
224
- end
225
-
226
- def link_support_dirs
227
- ['modules', 'templates'].each do |subdir|
228
- ln_sf(File.join(Atmos.config.root_dir, subdir), @working_dir)
229
- end
230
- end
231
-
232
- def link_shared_plugin_dir
233
- if ! Atmos.config["terraform.disable_shared_plugins"]
234
- shared_plugins_dir = File.join(Atmos.config.tmp_root, "terraform_plugins")
235
- mkdir_p(shared_plugins_dir)
236
- terraform_state_dir = File.join(tf_recipes_dir, '.terraform')
237
- mkdir_p(terraform_state_dir)
238
- terraform_plugins_dir = File.join(terraform_state_dir, 'plugins')
239
- ln_sf(shared_plugins_dir, terraform_plugins_dir)
240
- end
241
- end
242
-
243
- def link_recipes
244
- @recipes.each do |recipe|
245
- ln_sf(File.join(Atmos.config.root_dir, 'recipes', "#{recipe}.tf"), tf_recipes_dir)
246
- end
247
- end
248
-
249
- def pipe_stream(src, dest)
250
- Thread.new do
251
- block_size = 1024
252
- begin
253
- while data = src.readpartial(block_size)
254
- data = yield data if block_given?
255
- dest.write(data)
256
- end
257
- rescue EOFError
258
- nil
259
- rescue Exception => e
260
- logger.log_exception(e, "Stream failure")
261
- end
262
- end
263
- end
264
-
265
- end
266
-
267
- end
data/lib/atmos/ui.rb DELETED
@@ -1,159 +0,0 @@
1
- require_relative '../atmos'
2
- require 'highline'
3
- require 'rainbow'
4
- require 'yaml'
5
- require 'open3'
6
- require 'os'
7
- require 'hashie'
8
-
9
- module OSDockerDetection
10
- refine OS.singleton_class do
11
- def docker?
12
- @docker ||= File.exist?('/.dockerenv')
13
- end
14
- end
15
- end
16
-
17
- module Atmos
18
- module UI
19
- extend ActiveSupport::Concern
20
- include GemLogger::LoggerSupport
21
- using OSDockerDetection
22
-
23
- def self.color_enabled=(val)
24
- Rainbow.enabled = val
25
- end
26
-
27
- def self.color_enabled
28
- Rainbow.enabled
29
- end
30
-
31
- class Markup
32
-
33
- def initialize(color = nil)
34
- @color = color
35
- @atmos_ui = HighLine.new
36
- end
37
-
38
- def say(statement)
39
- statement = @color ? Rainbow(statement).send(@color) : statement
40
- @atmos_ui.say(statement)
41
- end
42
-
43
- def ask(question, answer_type=nil, &details)
44
- s = @color ? Rainbow(question).send(@color) : question
45
- @atmos_ui.ask(question, answer_type, &details)
46
- end
47
-
48
- def agree(question, character=nil, &details)
49
- s = @color ? Rainbow(question).send(@color) : question
50
- @atmos_ui.agree(question, character, &details)
51
- end
52
-
53
- end
54
-
55
- def warn
56
- return Markup.new(:yellow)
57
- end
58
-
59
- def error
60
- return Markup.new(:red)
61
- end
62
-
63
- def say(statement)
64
- return Markup.new().say(statement)
65
- end
66
-
67
- def ask(question, answer_type=nil, &details)
68
- return Markup.new().ask(question, answer_type, &details)
69
- end
70
-
71
- def agree(question, character=nil, &details)
72
- return Markup.new().agree(question, character, &details)
73
- end
74
-
75
- # Pretty display of hashes
76
- def display(data)
77
- data = Hashie.stringify_keys(data)
78
- display = YAML.dump(data).sub(/\A---\n/, "").gsub(/^/, " ")
79
- end
80
-
81
- def notify(message:nil, title: nil, modal: false, **opts)
82
-
83
- result = {
84
- 'stdout' => '',
85
- 'success' => ''
86
- }
87
-
88
- message = message.to_s
89
- title = title.present? ? title.to_s : "Atmos Notification"
90
- modal = ["true", "1"].include?(modal.to_s)
91
- modal = false if Atmos.config["ui.notify.disable_modal"]
92
-
93
- return result if Atmos.config["ui.notify.disable"].to_s == "true"
94
-
95
- force_inline = Atmos.config["ui.notify.force_inline"].to_s == "true"
96
- command = Atmos.config["ui.notify.command"]
97
-
98
- if command.present? && ! force_inline
99
-
100
- raise ArgumentError.new("notify command must be a list") if ! command.is_a?(Array)
101
-
102
- command = command.collect do |c|
103
- c = c.gsub("{{title}}", title)
104
- c = c.gsub("{{message}}", message)
105
- c = c.gsub("{{modal}}", modal.to_s)
106
- end
107
- result.merge! run_ui_process(*command)
108
-
109
- elsif OS.mac? && ! force_inline
110
- display_method = modal ? "displayDialog" : "displayNotification"
111
-
112
- dialogScript = <<~EOF
113
- var app = Application.currentApplication();
114
- app.includeStandardAdditions = true;
115
- app.#{display_method}(
116
- #{JSON.generate(message)}, {
117
- withTitle: #{JSON.generate(title)},
118
- buttons: ['OK'],
119
- defaultButton: 1
120
- })
121
- EOF
122
-
123
- result.merge! run_ui_process("osascript", "-l", "JavaScript", "-e", dialogScript)
124
-
125
- elsif OS.linux? && ! OS.docker? && ! force_inline
126
- # TODO: add a modal option
127
- result.merge! run_ui_process("notify-send", title, message)
128
-
129
- # TODO windows notifications?
130
- # elseif OS.windows? && ! force_inline
131
-
132
- else
133
-
134
- logger.debug("Notifications are unsupported on this OS")
135
- logger.info(Rainbow("\n***** #{title} *****\n#{message}\n").orange)
136
- if modal
137
- logger.info(Rainbow("Hit enter to continue\n").orange)
138
- $stdin.gets
139
- end
140
-
141
- end
142
-
143
- return result
144
- end
145
-
146
- private
147
-
148
- def run_ui_process(*args)
149
- stdout, status = Open3.capture2e(*args)
150
- result = {'stdout' => stdout, 'success' => status.success?.to_s}
151
- if ! status.success?
152
- result['error'] = "Notification process failed"
153
- logger.debug("Failed to run notification utility: #{stdout}")
154
- end
155
- return result
156
- end
157
-
158
- end
159
- end
data/lib/atmos/utils.rb DELETED
@@ -1,50 +0,0 @@
1
- require_relative '../atmos'
2
-
3
- module Atmos
4
- module Utils
5
-
6
- extend ActiveSupport::Concern
7
- include GemLogger::LoggerSupport
8
-
9
- class SymbolizedMash < ::Hashie::Mash
10
- include Hashie::Extensions::Mash::SymbolizeKeys
11
- end
12
-
13
- # remove leading whitespace using first non-empty line to determine how
14
- # much space to remove from the rest. Skips empty lines
15
- def clean_indent(str)
16
- first = true
17
- first_size = 0
18
- str.lines.collect do |line|
19
- if line =~ /^(\s*)\S/ # line has at least one non-whitespace character
20
- if first
21
- first_size = Regexp.last_match(0).size
22
- first = false
23
- end
24
- line[(first_size - 1)..-1]
25
- else
26
- line
27
- end
28
- end.join()
29
- end
30
-
31
- # wraps to an 80 character limit by adding newlines
32
- def wrap(str)
33
- result = ""
34
- count = 0
35
- str.each do |c|
36
- result << c
37
- if count >= 78
38
- result << "\n"
39
- count = 0
40
- else
41
- count += 1
42
- end
43
- end
44
- return result
45
- end
46
-
47
- extend self
48
-
49
- end
50
- end