simplygenius-atmos 0.7.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 (48) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +2 -0
  3. data/LICENSE +13 -0
  4. data/README.md +212 -0
  5. data/exe/atmos +4 -0
  6. data/exe/atmos-docker +12 -0
  7. data/lib/atmos.rb +12 -0
  8. data/lib/atmos/cli.rb +105 -0
  9. data/lib/atmos/commands/account.rb +65 -0
  10. data/lib/atmos/commands/apply.rb +20 -0
  11. data/lib/atmos/commands/auth_exec.rb +29 -0
  12. data/lib/atmos/commands/base_command.rb +12 -0
  13. data/lib/atmos/commands/bootstrap.rb +72 -0
  14. data/lib/atmos/commands/container.rb +58 -0
  15. data/lib/atmos/commands/destroy.rb +18 -0
  16. data/lib/atmos/commands/generate.rb +90 -0
  17. data/lib/atmos/commands/init.rb +18 -0
  18. data/lib/atmos/commands/new.rb +18 -0
  19. data/lib/atmos/commands/otp.rb +54 -0
  20. data/lib/atmos/commands/plan.rb +20 -0
  21. data/lib/atmos/commands/secret.rb +87 -0
  22. data/lib/atmos/commands/terraform.rb +52 -0
  23. data/lib/atmos/commands/user.rb +74 -0
  24. data/lib/atmos/config.rb +208 -0
  25. data/lib/atmos/exceptions.rb +9 -0
  26. data/lib/atmos/generator.rb +199 -0
  27. data/lib/atmos/generator_factory.rb +93 -0
  28. data/lib/atmos/ipc.rb +132 -0
  29. data/lib/atmos/ipc_actions/notify.rb +27 -0
  30. data/lib/atmos/ipc_actions/ping.rb +19 -0
  31. data/lib/atmos/logging.rb +160 -0
  32. data/lib/atmos/otp.rb +61 -0
  33. data/lib/atmos/provider_factory.rb +19 -0
  34. data/lib/atmos/providers/aws/account_manager.rb +82 -0
  35. data/lib/atmos/providers/aws/auth_manager.rb +208 -0
  36. data/lib/atmos/providers/aws/container_manager.rb +116 -0
  37. data/lib/atmos/providers/aws/provider.rb +51 -0
  38. data/lib/atmos/providers/aws/s3_secret_manager.rb +49 -0
  39. data/lib/atmos/providers/aws/user_manager.rb +211 -0
  40. data/lib/atmos/settings_hash.rb +90 -0
  41. data/lib/atmos/terraform_executor.rb +267 -0
  42. data/lib/atmos/ui.rb +159 -0
  43. data/lib/atmos/utils.rb +50 -0
  44. data/lib/atmos/version.rb +3 -0
  45. data/templates/new/config/atmos.yml +50 -0
  46. data/templates/new/config/atmos/runtime.yml +43 -0
  47. data/templates/new/templates.yml +1 -0
  48. metadata +526 -0
@@ -0,0 +1,52 @@
1
+ require_relative 'base_command'
2
+ require_relative '../../atmos/terraform_executor'
3
+
4
+ module Atmos::Commands
5
+
6
+ class Terraform < BaseCommand
7
+
8
+ def self.description
9
+ "Runs terraform"
10
+ end
11
+
12
+ # override so we can pass all options/flags/parameters directly to
13
+ # terraform instead of having clamp parse them
14
+ def parse(arguments)
15
+ @terraform_arguments = arguments
16
+ end
17
+
18
+ def execute
19
+
20
+ unless Atmos.config.is_atmos_repo?
21
+ signal_usage_error <<~EOF
22
+ Atmos can only run terraform from a location configured for atmos.
23
+ Have you run atmos init?"
24
+ EOF
25
+ end
26
+
27
+ Atmos.config.provider.auth_manager.authenticate(ENV) do |auth_env|
28
+ begin
29
+
30
+ # TODO: hack to allow apply/etc for bootstrap group
31
+ # Fix this once we allow more extensive recipe grouping
32
+ working_group = 'default'
33
+ @terraform_arguments.each_with_index do |a, i|
34
+ if a == "--group"
35
+ @terraform_arguments.delete_at(i)
36
+ working_group = @terraform_arguments.delete_at(i)
37
+ break
38
+ end
39
+ end
40
+
41
+ exe = Atmos::TerraformExecutor.new(process_env: auth_env, working_group: working_group)
42
+ get_modules = @terraform_arguments.delete("--get-modules")
43
+ exe.run(*@terraform_arguments, get_modules: get_modules.present?)
44
+ rescue Atmos::TerraformExecutor::ProcessFailed => e
45
+ logger.error(e.message)
46
+ end
47
+ end
48
+ end
49
+
50
+ end
51
+
52
+ end
@@ -0,0 +1,74 @@
1
+ require_relative 'base_command'
2
+ require 'climate_control'
3
+
4
+ module Atmos::Commands
5
+
6
+ class User < BaseCommand
7
+
8
+ def self.description
9
+ "Manages users in the cloud provider"
10
+ end
11
+
12
+ subcommand "create", "Create a new user" do
13
+
14
+ option ["-f", "--force"],
15
+ :flag, "forces deletion/updates for pre-existing resources\n",
16
+ default: false
17
+
18
+ option ["-l", "--login"],
19
+ :flag, "generate a login password\n",
20
+ default: false
21
+
22
+ option ["-m", "--mfa"],
23
+ :flag, "setup a mfa device\n",
24
+ default: false
25
+
26
+ option ["-k", "--key"],
27
+ :flag, "create access keys\n",
28
+ default: false
29
+
30
+ option ["-p", "--public-key"],
31
+ "PUBLIC_KEY", "add ssh public key\n"
32
+
33
+ option ["-g", "--group"],
34
+ "GROUP",
35
+ "associate the given groups to new user\n",
36
+ multivalued: true
37
+
38
+ parameter "USERNAME",
39
+ "The username of the user to add\nShould be an email address" do |u|
40
+ raise ArgumentError.new("Not an email") if u !~ URI::MailTo::EMAIL_REGEXP
41
+ u
42
+ end
43
+
44
+ def execute
45
+
46
+ Atmos.config.provider.auth_manager.authenticate(ENV) do |auth_env|
47
+ ClimateControl.modify(auth_env) do
48
+ manager = Atmos.config.provider.user_manager
49
+ user = manager.create_user(username)
50
+ user.merge!(manager.set_groups(username, group_list, force: force?)) if group_list.present?
51
+ user.merge!(manager.enable_login(username, force: force?)) if login?
52
+ user.merge!(manager.enable_mfa(username, force: force?)) if mfa?
53
+ user.merge!(manager.enable_access_keys(username, force: force?)) if key?
54
+ user.merge!(manager.set_public_key(username, public_key, force: force?)) if public_key.present?
55
+
56
+ logger.info "\nUser created:\n#{display user}\n"
57
+
58
+ if mfa? && user[:mfa_secret]
59
+ save_mfa = agree("Save the MFA secret for runtime integration with auth? ") {|q|
60
+ q.default = 'y'
61
+ }
62
+ Atmos::Otp.instance.save if save_mfa
63
+ end
64
+
65
+ end
66
+ end
67
+
68
+ end
69
+
70
+ end
71
+
72
+ end
73
+
74
+ end
@@ -0,0 +1,208 @@
1
+ require_relative '../atmos'
2
+ require_relative '../atmos/settings_hash'
3
+ require_relative '../atmos/provider_factory'
4
+ require 'yaml'
5
+ require 'fileutils'
6
+ require 'find'
7
+
8
+ module Atmos
9
+ class Config
10
+ include GemLogger::LoggerSupport
11
+ include FileUtils
12
+
13
+ attr_accessor :atmos_env, :root_dir,
14
+ :config_file, :configs_dir,
15
+ :tmp_root
16
+
17
+ def initialize(atmos_env)
18
+ @atmos_env = atmos_env
19
+ @root_dir = File.expand_path(Dir.pwd)
20
+ @config_file = File.join(root_dir, "config", "atmos.yml")
21
+ @configs_dir = File.join(root_dir, "config", "atmos")
22
+ @tmp_root = File.join(root_dir, "tmp")
23
+ end
24
+
25
+ def is_atmos_repo?
26
+ File.exist?(config_file)
27
+ end
28
+
29
+ def [](key)
30
+ load
31
+ result = @config.notation_get(key)
32
+ return result
33
+ end
34
+
35
+ def to_h
36
+ load
37
+ @config.to_hash
38
+ end
39
+
40
+ def provider
41
+ @provider ||= Atmos::ProviderFactory.get(self[:provider])
42
+ end
43
+
44
+ def all_env_names
45
+ load
46
+ @full_config[:environments].keys
47
+ end
48
+
49
+ def account_hash
50
+ load
51
+ environments = @full_config[:environments] || {}
52
+ environments.inject(Hash.new) do |accum, entry|
53
+ accum[entry.first] = entry.last[:account_id]
54
+ accum
55
+ end
56
+ end
57
+
58
+ def tmp_dir
59
+ @tmp_dir ||= begin
60
+ dir = File.join(tmp_root, atmos_env)
61
+ logger.debug("Tmp dir: #{dir}")
62
+ mkdir_p(dir)
63
+ dir
64
+ end
65
+ end
66
+
67
+ def auth_cache_dir
68
+ @auth_cache_dir ||= begin
69
+ dir = File.join(tmp_dir, 'auth')
70
+ logger.debug("Auth cache dir: #{dir}")
71
+ mkdir_p(dir)
72
+ dir
73
+ end
74
+ end
75
+
76
+ def tf_working_dir(group='default')
77
+ @tf_working_dir ||= {}
78
+ @tf_working_dir[group] ||= begin
79
+ dir = File.join(tmp_dir, 'tf', group)
80
+ logger.debug("Terraform working dir: #{dir}")
81
+ mkdir_p(dir)
82
+ dir
83
+ end
84
+ end
85
+
86
+ private
87
+
88
+ INTERP_PATTERN = /(\#\{([^\}]+)\})/
89
+
90
+ def load
91
+ @config ||= begin
92
+
93
+ logger.debug("Atmos env: #{atmos_env}")
94
+
95
+ if ! File.exist?(config_file)
96
+ logger.warn "Could not find an atmos config file at: #{config_file}"
97
+ # raise RuntimeError.new("Could not find an atmos config file at: #{config_file}")
98
+ end
99
+
100
+ logger.debug("Loading atmos config file #{config_file}")
101
+ @full_config = SettingsHash.new((YAML.load_file(config_file) rescue Hash.new))
102
+
103
+ if Dir.exist?(configs_dir)
104
+ logger.debug("Loading atmos config files from #{configs_dir}")
105
+ Find.find(configs_dir) do |f|
106
+ if f =~ /\.ya?ml/i
107
+ logger.debug("Loading atmos config file: #{f}")
108
+ h = SettingsHash.new(YAML.load_file(f))
109
+ @full_config = @full_config.merge(h)
110
+ end
111
+ end
112
+ else
113
+ logger.debug("Atmos config dir doesn't exist: #{configs_dir}")
114
+ end
115
+
116
+ @full_config['provider'] = provider_name = @full_config['provider'] || 'aws'
117
+ global = SettingsHash.new(@full_config.reject {|k, v| ['environments', 'providers'].include?(k) })
118
+ begin
119
+ prov = @full_config.deep_fetch(:providers, provider_name)
120
+ rescue
121
+ logger.debug("No provider config found for '#{provider_name}'")
122
+ prov = {}
123
+ end
124
+
125
+ begin
126
+ env = @full_config.deep_fetch(:environments, atmos_env)
127
+ rescue
128
+ logger.debug("No environment config found for '#{atmos_env}'")
129
+ env = {}
130
+ end
131
+
132
+ conf = global.deep_merge(prov).
133
+ deep_merge(env).
134
+ deep_merge(
135
+ atmos_env: atmos_env,
136
+ atmos_version: Atmos::VERSION
137
+ )
138
+ expand(conf, conf)
139
+ end
140
+ end
141
+
142
+ def expand(config, obj)
143
+ case obj
144
+ when Hash
145
+ SettingsHash.new(Hash[obj.collect {|k, v| [k, expand(config, v)] }])
146
+ when Array
147
+ obj.collect {|i| expand(config, i) }
148
+ when String
149
+ result = obj
150
+ result.scan(INTERP_PATTERN).each do |substr, statement|
151
+ # TODO: check for cycles
152
+ if statement =~ /^[\w\.\[\]]$/
153
+ val = config.notation_get(statement)
154
+ else
155
+ # TODO: be consistent with dot notation between eval and
156
+ # notation_get. eval ends up calling Hashie method_missing,
157
+ # which returns nil if a key doesn't exist, causing a nil
158
+ # exception for next item in chain, while notation_get returns
159
+ # nil gracefully for the entire chain (preferred)
160
+ begin
161
+ val = eval(statement, config.instance_eval("binding"))
162
+ rescue => e
163
+ file, line = find_config_error(substr)
164
+ file_msg = file.nil? ? "" : " in #{File.basename(file)}:#{line}"
165
+ raise RuntimeError.new("Failing config statement '#{substr}'#{file_msg} => #{e.class} #{e.message}")
166
+ end
167
+ end
168
+ result = result.sub(substr, expand(config, val).to_s)
169
+ end
170
+ result = true if result == 'true'
171
+ result = false if result == 'false'
172
+ result
173
+ else
174
+ obj
175
+ end
176
+ end
177
+
178
+ def find_config_error(statement)
179
+ filename = nil
180
+ line = 0
181
+
182
+ configs = []
183
+ configs << config_file if File.exist?(config_file)
184
+ if Dir.exist?(configs_dir)
185
+ Find.find(configs_dir) do |f|
186
+ if f =~ /\.ya?ml/i
187
+ configs << f
188
+ end
189
+ end
190
+ end
191
+
192
+ configs.each do |c|
193
+ current_line = 0
194
+ File.foreach(c) do |f|
195
+ current_line += 1
196
+ if f.include?(statement)
197
+ filename = c
198
+ line = current_line
199
+ break
200
+ end
201
+ end
202
+ end
203
+
204
+ return filename, line
205
+ end
206
+ end
207
+
208
+ end
@@ -0,0 +1,9 @@
1
+ require_relative '../atmos'
2
+
3
+ module Atmos
4
+ module Exceptions
5
+ class UsageError < Clamp::UsageError
6
+
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,199 @@
1
+ require_relative '../atmos'
2
+ require_relative '../atmos/ui'
3
+ require 'thor'
4
+ require 'find'
5
+
6
+ module Atmos
7
+
8
+ # From https://github.com/rubber/rubber/blob/master/lib/rubber/commands/vulcanize.rb
9
+ class Generator < Thor
10
+
11
+ include Thor::Actions
12
+
13
+ def initialize(*args, **opts)
14
+ super
15
+ @dependencies = opts[:dependencies]
16
+ end
17
+
18
+ no_commands do
19
+
20
+ include GemLogger::LoggerSupport
21
+ include Atmos::UI
22
+
23
+ TEMPLATES_SPEC_FILE = 'templates.yml'
24
+ TEMPLATES_ACTIONS_FILE = 'templates.rb'
25
+
26
+ def self.valid_templates
27
+ all_entries = []
28
+ source_paths_for_search.collect do |path|
29
+ entries = []
30
+ if Dir.exist?(path)
31
+ Find.find(path) do |f|
32
+ Find.prune if File.basename(f) =~ /(^\.)|svn|CVS/
33
+
34
+ template_spec = File.join(f, TEMPLATES_SPEC_FILE)
35
+ if File.exist?(template_spec)
36
+ entries << f.sub(/^#{path}\//, '')
37
+ Find.prune
38
+ end
39
+ end
40
+ all_entries << entries.sort
41
+ else
42
+ logger.warn("Sourcepath does not exist: #{path}")
43
+ end
44
+ end
45
+
46
+ return all_entries.flatten
47
+ end
48
+
49
+ def valid_templates
50
+ self.class.valid_templates
51
+ end
52
+
53
+ def generate(template_names)
54
+ seen = Set.new
55
+ Array(template_names).each do |template_name|
56
+ template_dependencies = find_dependencies(template_name)
57
+ template_dependencies << template_name
58
+ template_dependencies.each do |tname|
59
+ apply_template(tname) unless seen.include?(tname)
60
+ seen << tname
61
+ end
62
+ end
63
+ end
64
+
65
+ end
66
+
67
+ protected
68
+
69
+ def template_dir(name)
70
+ template_dir = nil
71
+ source_path = nil
72
+ source_paths.each do |sp|
73
+ potential_template_dir = File.join(sp, name, '')
74
+ template_spec = File.join(sp, name, TEMPLATES_SPEC_FILE)
75
+ if File.exist?(template_spec) && File.directory?(potential_template_dir)
76
+ template_dir = potential_template_dir
77
+ source_path = sp
78
+ break
79
+ end
80
+ end
81
+
82
+ unless template_dir.present?
83
+ raise ArgumentError.new("Invalid template #{name}, use one of: #{valid_templates.join(', ')}")
84
+ end
85
+
86
+ return template_dir, source_path
87
+ end
88
+
89
+ def find_dependencies(name, seen=[])
90
+ template_dir, source_path = template_dir(name)
91
+
92
+ return [] unless @dependencies
93
+
94
+ if seen.include?(name)
95
+ seen << name
96
+ raise ArgumentError.new("Circular template dependency: #{seen.to_a.join(" => ")}")
97
+ end
98
+ seen << name
99
+
100
+ template_conf = load_template_config(template_dir)
101
+ template_dependencies = Set.new(Array(template_conf['dependent_templates'] || []))
102
+
103
+ template_dependencies.clone.each do |dep|
104
+ template_dependencies.merge(find_dependencies(dep, seen.dup))
105
+ end
106
+
107
+ return template_dependencies.to_a
108
+ end
109
+
110
+ def apply_template(name)
111
+ template_dir, source_path = template_dir(name)
112
+ logger.debug("Applying template '#{name}' from '#{template_dir}' in sourcepath '#{source_path}'")
113
+ template_conf = load_template_config(template_dir)
114
+
115
+ extra_generator_steps_file = File.join(template_dir, TEMPLATES_ACTIONS_FILE)
116
+
117
+ Find.find(template_dir) do |f|
118
+ Find.prune if f == File.join(template_dir, TEMPLATES_SPEC_FILE) # don't copy over templates.yml
119
+ Find.prune if f == extra_generator_steps_file # don't copy over templates.rb
120
+
121
+ # Using File.join(x, '') to ensure trailing slash to make sure we end
122
+ # up with a relative path
123
+ template_rel = f.gsub(/#{File.join(template_dir, '')}/, '')
124
+ source_rel = f.gsub(/#{File.join(source_path, '')}/, '')
125
+ dest_rel = source_rel.gsub(/^#{File.join(name, '')}/, '')
126
+
127
+ # prune non-directories at top level (the top level directory is the
128
+ # template dir itself)
129
+ if f !~ /\// && ! File.directory?(f)
130
+ Find.prune
131
+ end
132
+
133
+ # Only include optional files when their conditions eval to true
134
+ optional = template_conf['optional'][template_rel] rescue nil
135
+ if optional
136
+ exclude = ! eval(optional)
137
+ logger.debug("Optional template '#{template_rel}' with condition: '#{optional}', excluding=#{exclude}")
138
+ Find.prune if exclude
139
+ end
140
+
141
+ logger.debug("Template '#{source_rel}' => '#{dest_rel}'")
142
+ if File.directory?(f)
143
+ empty_directory(dest_rel)
144
+ else
145
+ copy_file(source_rel, dest_rel, mode: :preserve)
146
+ end
147
+ end
148
+
149
+ if File.exist? extra_generator_steps_file
150
+ eval File.read(extra_generator_steps_file), binding, extra_generator_steps_file
151
+ end
152
+ end
153
+
154
+ def load_template_config(template_dir)
155
+ YAML.load(File.read(File.join(template_dir, 'templates.yml'))) || {} rescue {}
156
+ end
157
+
158
+ def raw_config(yml_file)
159
+ @raw_configs ||= {}
160
+ @raw_configs[yml_file] ||= SettingsHash.new((YAML.load_file(yml_file) rescue {}))
161
+ end
162
+
163
+ def add_config(yml_file, key, value, additive: true)
164
+ new_yml = SettingsHash.add_config(yml_file, key, value, additive: additive)
165
+ create_file yml_file, new_yml
166
+ @raw_configs.delete(yml_file) if @raw_configs
167
+ end
168
+
169
+ def get_config(yml_file, key)
170
+ config = raw_config(yml_file)
171
+ config.notation_get(key)
172
+ end
173
+
174
+ def config_present?(yml_file, key, value=nil)
175
+ val = get_config(yml_file, key)
176
+
177
+ result = val.present?
178
+ if value && result
179
+ if val.is_a?(Array)
180
+ result = Array(value).all? {|v| val.include?(v) }
181
+ else
182
+ result = (val == value)
183
+ end
184
+ end
185
+
186
+ return result
187
+ end
188
+
189
+ # TODO make a context object for these actions, and populate it with things
190
+ # like template_dir from within apply
191
+ def new_keys?(src_yml_file, dest_yml_file)
192
+ src = raw_config(src_yml_file).keys.sort
193
+ dest = raw_config(dest_yml_file).keys.sort
194
+ (src - dest).size > 0
195
+ end
196
+
197
+ end
198
+
199
+ end