simplygenius-atmos 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
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