simplygenius-atmos 0.7.1 → 0.8.0

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 (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