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
@@ -0,0 +1,120 @@
1
+ require_relative '../atmos'
2
+ require_relative 'plugin'
3
+ require 'set'
4
+
5
+ module SimplyGenius
6
+ module Atmos
7
+
8
+ class PluginManager
9
+ include GemLogger::LoggerSupport
10
+
11
+ attr_reader :plugins
12
+
13
+ def initialize(plugins)
14
+ @plugins = []
15
+ Array(plugins).each do |plugin|
16
+ if plugin.is_a?(String)
17
+ name = plugin
18
+ plugin = SettingsHash.new
19
+ plugin[:name] = name
20
+ elsif plugin.is_a?(Hash)
21
+ plugin = SettingsHash.new(plugin)
22
+ if plugin[:name].blank?
23
+ logger.error "Invalid plugin definition, :name missing: #{plugin}"
24
+ next
25
+ end
26
+ else
27
+ logger.error "Invalid plugin definition: #{plugin}"
28
+ next
29
+ end
30
+ @plugins << plugin
31
+ end
32
+
33
+ @plugin_classes = Set.new
34
+ @plugin_instances = []
35
+ @output_filters = {}
36
+ end
37
+
38
+ def load_plugins
39
+ @plugins.each do |plugin|
40
+ load_plugin(plugin)
41
+
42
+ # Check for new plugin classes after each plugin load so that we can
43
+ # initialize them with their own config hash
44
+ Plugin.descendants.each do |plugin_class|
45
+ begin
46
+ if ! @plugin_classes.include?(plugin_class)
47
+ @plugin_classes << plugin_class
48
+ @plugin_instances << plugin_class.new(plugin)
49
+ end
50
+ rescue StandardError => e
51
+ logger.log_exception e, "Failed to initialize plugin: #{plugin_class}"
52
+ end
53
+ end
54
+ end
55
+ end
56
+
57
+ def load_plugin(plugin)
58
+ begin
59
+ name = plugin[:name]
60
+ require_name = plugin[:require] || name.gsub('-', '/')
61
+ logger.debug("Loading plugin #{name} as #{require_name}")
62
+ require require_name
63
+ rescue LoadError, StandardError => e
64
+ logger.log_exception e, "Failed to load atmos plugin: #{name} - #{e.message}"
65
+ end
66
+ end
67
+
68
+ def validate_output_filter_type(type)
69
+ raise "Invalid output filter type #{type}, must be one of [:stdout, :stderr]" unless [:stdout, :stderr].include?(type)
70
+ end
71
+
72
+ def register_output_filter(type, clazz)
73
+ validate_output_filter_type(type)
74
+ @output_filters[type.to_sym] ||= []
75
+ @output_filters[type.to_sym] << clazz
76
+ end
77
+
78
+ def output_filters(type, context)
79
+ validate_output_filter_type(type)
80
+ @output_filters[type.to_sym] ||= []
81
+ return OutputFilterCollection.new(@output_filters[type.to_sym].collect {|clazz| clazz.new(context) })
82
+ end
83
+
84
+ class OutputFilterCollection
85
+ include GemLogger::LoggerSupport
86
+
87
+ attr_accessor :filters
88
+
89
+ def initialize(filters)
90
+ @filters = filters
91
+ end
92
+
93
+ def filter_block
94
+ return Proc.new do |data|
95
+ @filters.inject(data) do |memo, obj|
96
+ begin
97
+ obj.filter(memo)
98
+ rescue StandardError => e
99
+ logger.log_exception e, "Output filter failed during filter: #{obj.class}"
100
+ memo
101
+ end
102
+ end
103
+ end
104
+ end
105
+
106
+ def close
107
+ @filters.each do |f|
108
+ begin
109
+ f.close
110
+ rescue StandardError => e
111
+ logger.log_exception e, "Output filter failed during close: #{f.class}"
112
+ end
113
+ end
114
+ end
115
+
116
+ end
117
+ end
118
+
119
+ end
120
+ end
@@ -0,0 +1,29 @@
1
+ require_relative '../../atmos'
2
+ require_relative '../../atmos/ui'
3
+
4
+ module SimplyGenius
5
+ module Atmos
6
+ module Plugins
7
+
8
+ class OutputFilter
9
+ include GemLogger::LoggerSupport
10
+ include UI
11
+
12
+ attr_reader :context
13
+
14
+ def initialize(context)
15
+ @context = context
16
+ end
17
+
18
+ def filter(data)
19
+ raise "not implemented"
20
+ end
21
+
22
+ def close
23
+ end
24
+
25
+ end
26
+
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,21 @@
1
+ require_relative '../../atmos'
2
+ require_relative 'output_filter'
3
+
4
+ module SimplyGenius
5
+ module Atmos
6
+ module Plugins
7
+
8
+ class PromptNotify < OutputFilter
9
+
10
+ def filter(data)
11
+ if data =~ /^[\e\[\dm\s]*Enter a value:[\e\[\dm\s]*$/
12
+ notify(message: "Terraform is waiting for user input")
13
+ end
14
+ data
15
+ end
16
+
17
+ end
18
+
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,23 @@
1
+ require_relative '../atmos'
2
+
3
+ module SimplyGenius
4
+ module Atmos
5
+
6
+ class ProviderFactory
7
+ include GemLogger::LoggerSupport
8
+
9
+ def self.get(name)
10
+ @provider ||= begin
11
+ logger.debug("Loading provider: #{name}")
12
+ require "simplygenius/atmos/providers/#{name}/provider"
13
+ provider = "SimplyGenius::Atmos::Providers::#{name.camelize}::Provider".constantize
14
+ logger.debug("Loaded provider #{provider}")
15
+ provider.new(name)
16
+ end
17
+ return @provider
18
+ end
19
+
20
+ end
21
+
22
+ end
23
+ end
@@ -0,0 +1,83 @@
1
+ require_relative '../../../atmos'
2
+ require 'aws-sdk-organizations'
3
+
4
+ module SimplyGenius
5
+ module Atmos
6
+ module Providers
7
+ module Aws
8
+
9
+ class AccountManager
10
+ include GemLogger::LoggerSupport
11
+ include FileUtils
12
+
13
+ def initialize(provider)
14
+ @provider = provider
15
+ end
16
+
17
+ def create_account(env, name: nil, email: nil)
18
+ result = {}
19
+ org = ::Aws::Organizations::Client.new
20
+ resp = nil
21
+ name ||= "Atmos #{env} account"
22
+
23
+ begin
24
+ logger.info "Looking up organization"
25
+ resp = org.describe_organization()
26
+ logger.debug "Described organization: #{resp.to_h}"
27
+ rescue ::Aws::Organizations::Errors::AWSOrganizationsNotInUseException
28
+ logger.info "Organization doesn't exist, creating"
29
+ resp = org.create_organization()
30
+ logger.debug "Created organization: #{resp.to_h}"
31
+ end
32
+
33
+ if email.blank?
34
+ master_email = resp.organization.master_account_email
35
+ email = master_email.sub('@', "+#{env}@")
36
+ end
37
+ result[:email] = email
38
+ result[:name] = name
39
+
40
+
41
+ begin
42
+ logger.info "Creating account named #{name}"
43
+ resp = org.create_account(account_name: name, email: email)
44
+ rescue ::Aws::Organizations::Errors::FinalizingOrganizationException
45
+ logger.info "Waiting to retry account creation as the organization needs to finalize"
46
+ logger.info "This will eventually succeed after receiving a"
47
+ logger.info "'Consolidated Billing verification' email from AWS"
48
+ logger.info "You can leave this running or cancel and restart later."
49
+ sleep 60
50
+ retry
51
+ end
52
+
53
+ logger.debug "Created account: #{resp.to_h}"
54
+
55
+ status_id = resp.create_account_status.id
56
+ status = resp.create_account_status.state
57
+ account_id = resp.create_account_status.account_id
58
+
59
+ while status =~ /in_progress/i
60
+ logger.info "Waiting for account creation to complete, status: #{status}"
61
+ resp = org.describe_create_account_status(create_account_request_id: status_id)
62
+ logger.debug("Account creation status check: #{resp.to_h}")
63
+ status = resp.create_account_status.state
64
+ account_id = resp.create_account_status.account_id
65
+ sleep 5
66
+ end
67
+
68
+ if status =~ /failed/i
69
+ logger.error "Failed to create account: #{resp.create_account_status.failure_reason}"
70
+ exit(1)
71
+ end
72
+
73
+ result[:account_id] = account_id
74
+
75
+ return result
76
+ end
77
+
78
+ end
79
+
80
+ end
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,220 @@
1
+ require_relative '../../../atmos'
2
+ require_relative '../../../atmos/utils'
3
+ require_relative '../../../atmos/ui'
4
+ require_relative '../../../atmos/otp'
5
+ require 'aws-sdk-core'
6
+ require 'aws-sdk-iam'
7
+ require 'json'
8
+
9
+ module SimplyGenius
10
+ module Atmos
11
+ module Providers
12
+ module Aws
13
+
14
+ class AuthManager
15
+ include GemLogger::LoggerSupport
16
+ include FileUtils
17
+ include UI
18
+
19
+ def initialize(provider)
20
+ @provider = provider
21
+ end
22
+
23
+ def authenticate(system_env, **opts, &block)
24
+
25
+ profile = system_env['AWS_PROFILE']
26
+ key = system_env['AWS_ACCESS_KEY_ID']
27
+ secret = system_env['AWS_SECRET_ACCESS_KEY']
28
+ if profile.blank? && (key.blank? || secret.blank?)
29
+ logger.warn("An aws profile or key/secret should be supplied via the environment")
30
+ end
31
+
32
+ # Handle bootstrapping a new env account. Newly created organization
33
+ # accounts only have the default role that can only be assumed by an
34
+ # iam user, so use that as the target for assume_role, and the root
35
+ # check below will ensure iam user
36
+ assume_role_name = nil
37
+ if opts[:bootstrap] && Atmos.config.atmos_env != 'ops'
38
+ # TODO: do this hack better
39
+ assume_role_name = Atmos.config["auth.bootstrap_assume_role_name"]
40
+ else
41
+ assume_role_name = opts[:role] || Atmos.config["auth.assume_role_name"]
42
+ end
43
+ account_id = Atmos.config.account_hash[Atmos.config.atmos_env].to_s
44
+ role_arn = "arn:aws:iam::#{account_id}:role/#{assume_role_name}"
45
+
46
+ user_name = nil
47
+ begin
48
+ sts = ::Aws::STS::Client.new
49
+ resp = sts.get_caller_identity
50
+ arn_pieces = resp.arn.split(":")
51
+ user_name = arn_pieces.last.split("/").last
52
+
53
+ # root credentials can't assume role, but they should have full
54
+ # access for the current account, so proceed (e.g. for bootstrap).
55
+ if arn_pieces.last == "root"
56
+
57
+ # We check the account of the caller to prevent root user of ops
58
+ # account from bootstrapping an env account, but still allow a
59
+ # root user of the env account itself to be able to bootstrap
60
+ # (i.e. to allow not organizational accounts to bootstrap using
61
+ # their root user)
62
+ if arn_pieces[-2] != account_id
63
+ logger.error <<~EOF
64
+ Account doesn't match credentials. Bootstrapping a new
65
+ account should be done as an iam user from the ops account or
66
+ using credentials for a root user of the env account.
67
+ EOF
68
+ exit(1)
69
+ end
70
+
71
+ # Should only use root credentials for bootstrap, and thus we
72
+ # won't have role requirement for mfa, etc, even if root account
73
+ # uses mfa for login. Thus skip all the other stuff, to
74
+ # encourage/force use of non-root accounts for normal use
75
+ logger.warn("Using aws root credentials - should only be neccessary for bootstrap")
76
+ return block.call(Hash[system_env])
77
+ end
78
+
79
+ rescue ::Aws::STS::Errors::ServiceError => e
80
+ logger.error "Could not discover aws credentials"
81
+ exit(1)
82
+ end
83
+
84
+ auth_needed = true
85
+ cache_key = "#{user_name}-#{assume_role_name}"
86
+ credentials = read_auth_cache[cache_key]
87
+
88
+ if credentials.present?
89
+ logger.debug("Session cache present, checking expiration...")
90
+ expiration = Time.parse(credentials['expiration'])
91
+ session_renew_interval = (session_duration / 4).to_i
92
+
93
+ if Time.now > expiration
94
+ logger.debug "Session cache is expired, performing normal auth"
95
+ auth_needed = true
96
+ elsif Time.now > (expiration - session_renew_interval)
97
+ begin
98
+ # TODO: investigate making all info a warn so we don't pollute stdout for shell scripts
99
+ logger.info "Session approaching expiration, renewing..."
100
+ credentials = assume_role(role_arn, credentials: credentials, user_name: user_name)
101
+ write_auth_cache(cache_key => credentials)
102
+ auth_needed = false
103
+ rescue => e
104
+ logger.info "Failed to renew credentials using session cache, reason: #{e.message}"
105
+ auth_needed = true
106
+ end
107
+ else
108
+ logger.debug "Session cache is current, skipping auth"
109
+ auth_needed = false
110
+ end
111
+ end
112
+
113
+ if auth_needed
114
+ begin
115
+ logger.info "No active session cache, authenticating..."
116
+
117
+ credentials = assume_role(role_arn, user_name: user_name)
118
+ write_auth_cache(cache_key => credentials)
119
+
120
+ rescue ::Aws::STS::Errors::AccessDenied => e
121
+ if e.message !~ /explicit deny/
122
+ logger.debug "Access Denied, reason: #{e.message}"
123
+ end
124
+
125
+ logger.info "Normal auth failed, checking for mfa"
126
+
127
+ iam = ::Aws::IAM::Client.new
128
+ response = iam.list_mfa_devices(user_name: user_name)
129
+ mfa_serial = response.mfa_devices.first.try(:serial_number)
130
+ token = nil
131
+ if mfa_serial.present?
132
+
133
+ token = Otp.instance.generate(user_name)
134
+ if token.nil?
135
+ token = ask("Enter token to retry with mfa: ")
136
+ else
137
+ logger.info "Used integrated atmos mfa to generate token"
138
+ end
139
+
140
+ if token.blank?
141
+ logger.error "A MFA token must be supplied"
142
+ exit(1)
143
+ end
144
+
145
+ else
146
+ logger.error "MFA is not setup for your account, retry after doing so"
147
+ exit(1)
148
+ end
149
+
150
+ credentials = assume_role(role_arn, serial_number: mfa_serial, token_code: token, user_name: user_name)
151
+ write_auth_cache(cache_key => credentials)
152
+
153
+ end
154
+ end
155
+
156
+ process_env = {}
157
+ process_env['AWS_ACCESS_KEY_ID'] = credentials['access_key_id']
158
+ process_env['AWS_SECRET_ACCESS_KEY'] = credentials['secret_access_key']
159
+ process_env['AWS_SESSION_TOKEN'] = credentials['session_token']
160
+ logger.debug("Calling authentication target with env: #{process_env.inspect}")
161
+ block.call(Hash[system_env].merge(process_env))
162
+ end
163
+
164
+ private
165
+
166
+ def session_duration
167
+ @session_duration ||= (Atmos.config["auth.session_duration"] || 3600).to_i
168
+ end
169
+
170
+ def assume_role(role_arn, **opts)
171
+ # use Aws::AssumeRoleCredentials ?
172
+ if opts[:credentials]
173
+ c = opts.delete(:credentials)
174
+ creds = ::Aws::Credentials.new(
175
+ c[:access_key_id], c[:secret_access_key], c[:session_token]
176
+ )
177
+ client_opts = {credentials: creds}
178
+ else
179
+ client_opts = {}
180
+ end
181
+
182
+ user_name = opts.delete(:user_name)
183
+ if user_name
184
+ session_name = "Atmos-#{user_name}"
185
+ else
186
+ session_name = "Atmos"
187
+ end
188
+
189
+ sts = ::Aws::STS::Client.new(client_opts)
190
+ params = {
191
+ duration_seconds: session_duration,
192
+ role_session_name: session_name,
193
+ role_arn: role_arn
194
+ }.merge(opts)
195
+ logger.debug("Assuming role: #{params}")
196
+ resp = sts.assume_role(params)
197
+ return Utils::SymbolizedMash.new(resp.credentials.to_h)
198
+ end
199
+
200
+ def auth_cache_file
201
+ File.join(Atmos.config.auth_cache_dir, 'aws-assume-role.json')
202
+ end
203
+
204
+ def write_auth_cache(h)
205
+ File.open(auth_cache_file, 'w') do |f|
206
+ f.puts(JSON.pretty_generate(h))
207
+ end
208
+ end
209
+
210
+ def read_auth_cache
211
+ data = JSON.parse(File.read(auth_cache_file)) rescue {}
212
+ Utils::SymbolizedMash.new(data)
213
+ end
214
+
215
+ end
216
+
217
+ end
218
+ end
219
+ end
220
+ end