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,19 +0,0 @@
1
- require_relative '../../atmos'
2
- require 'open3'
3
- require 'os'
4
-
5
- module Atmos
6
- module IpcActions
7
- class Ping
8
- include GemLogger::LoggerSupport
9
-
10
- def initialize()
11
- end
12
-
13
- def execute(**opts)
14
- return opts.merge(action: 'pong')
15
- end
16
-
17
- end
18
- end
19
- end
data/lib/atmos/logging.rb DELETED
@@ -1,160 +0,0 @@
1
- require 'logging'
2
- require 'gem_logger'
3
- require 'rainbow'
4
- require 'delegate'
5
-
6
- module Atmos
7
- module Logging
8
-
9
- module GemLoggerConcern
10
- extend ActiveSupport::Concern
11
-
12
- def logger
13
- ::Logging.logger[self.class]
14
- end
15
-
16
- module ClassMethods
17
- def logger
18
- ::Logging.logger[self]
19
- end
20
- end
21
- end
22
-
23
- class CaptureStream < SimpleDelegator
24
-
25
- def initialize(logger_name, appender, stream, color=nil)
26
- super(stream)
27
- @color = stream.tty? && color ? color : nil
28
- @logger = ::Logging.logger[logger_name]
29
- @logger.appenders = [appender]
30
- @logger.additive = false
31
- end
32
-
33
- def strip_color(str)
34
- str.gsub(/\e\[\d+m/, '')
35
- end
36
-
37
- def write(data)
38
- @logger.info(strip_color(data))
39
- if @color
40
- count = 0
41
- d = data.lines.each do |l|
42
- cl = Kernel.send(:Rainbow, l).send(@color)
43
- count += super(cl)
44
- end
45
- return count
46
- else
47
- return super(data)
48
- end
49
- end
50
- end
51
-
52
- def self.init_logger
53
- return if @initialized
54
- @initialized = true
55
-
56
- ::Logging.format_as :inspect
57
- ::Logging.backtrace true
58
-
59
- ::Logging.color_scheme(
60
- 'bright',
61
- lines: {
62
- debug: :green,
63
- info: :default,
64
- warn: :yellow,
65
- error: :red,
66
- fatal: [:white, :on_red]
67
- },
68
- date: :blue,
69
- logger: :cyan,
70
- message: :magenta
71
- )
72
-
73
- ::Logging.logger.root.level = :info
74
- GemLogger.configure do |config|
75
- config.default_logger = ::Logging.logger.root
76
- config.logger_concern = Atmos::Logging::GemLoggerConcern
77
- end
78
- end
79
-
80
-
81
- def self.testing
82
- @t
83
- end
84
-
85
- def self.testing=(t)
86
- @t = t
87
- end
88
-
89
- def self.sio
90
- ::Logging.logger.root.appenders.find {|a| a.name == 'sio' }
91
- end
92
-
93
- def self.contents
94
- sio.try(:sio).try(:to_s)
95
- end
96
-
97
- def self.clear
98
- sio.try(:clear)
99
- end
100
-
101
- def self.setup_logging(debug, color, logfile)
102
- init_logger
103
-
104
- ::Logging.logger.root.level = debug ? :debug : :info
105
- appenders = []
106
- detail_pattern = '[%d] %-5l %c{2} %m\n'
107
- plain_pattern = '%m\n'
108
-
109
- pattern_options = {
110
- pattern: plain_pattern
111
- }
112
- if color
113
- pattern_options[:color_scheme] = 'bright'
114
- end
115
-
116
- if self.testing
117
-
118
- appender = ::Logging.appenders.string_io(
119
- 'sio',
120
- layout: ::Logging.layouts.pattern(pattern_options)
121
- )
122
- appenders << appender
123
-
124
- else
125
-
126
- appender = ::Logging.appenders.stdout(
127
- 'stdout',
128
- layout: ::Logging.layouts.pattern(pattern_options)
129
- )
130
- appenders << appender
131
-
132
- end
133
-
134
- # Do this after setting up stdout appender so we don't duplicate output
135
- # to stdout with our capture
136
- if logfile.present?
137
-
138
- appender = ::Logging.appenders.file(
139
- logfile,
140
- truncate: true,
141
- layout: ::Logging.layouts.pattern(pattern: detail_pattern)
142
- )
143
- appenders << appender
144
-
145
- if ! $stdout.is_a? CaptureStream
146
- $stdout = CaptureStream.new("stdout", appender, $stdout)
147
- $stderr = CaptureStream.new("stderr", appender, $stderr, :red)
148
- silence_warnings {
149
- Object.const_set(:STDOUT, $stdout)
150
- Object.const_set(:STDERR, $stderr)
151
- }
152
- end
153
-
154
- end
155
-
156
- ::Logging.logger.root.appenders = appenders
157
- end
158
-
159
- end
160
- end
data/lib/atmos/otp.rb DELETED
@@ -1,61 +0,0 @@
1
- require_relative '../atmos'
2
- require 'singleton'
3
- require 'rotp'
4
-
5
- module Atmos
6
-
7
- class Otp
8
- include Singleton
9
- include GemLogger::LoggerSupport
10
-
11
- def initialize
12
- @secret_file = Atmos.config["otp.secret_file"] || "~/.atmos.yml"
13
- @secret_file = File.expand_path(@secret_file)
14
- yml_hash = YAML.load_file(@secret_file) rescue Hash.new
15
- @secret_store = SettingsHash.new(yml_hash)
16
- @secret_store[Atmos.config[:org]] ||= {}
17
- @secret_store[Atmos.config[:org]][:otp] ||= {}
18
- @scoped_secret_store = @secret_store[Atmos.config[:org]][:otp]
19
- end
20
-
21
- def add(name, secret)
22
- old = @scoped_secret_store[name]
23
- logger.info "Replacing OTP secret #{name}=#{old}" if old
24
- @scoped_secret_store[name] = secret
25
- end
26
-
27
- def remove(name)
28
- old = @scoped_secret_store.delete(name)
29
- @otp.try(:delete, name)
30
- logger.info "Removed OTP secret #{name}=#{old}" if old
31
- end
32
-
33
- def save
34
- File.write(@secret_file, YAML.dump(@secret_store.to_hash))
35
- File.chmod(0600, @secret_file)
36
- end
37
-
38
- def generate(name)
39
- otp(name).try(:now)
40
- end
41
-
42
- private
43
-
44
- def otp(name)
45
- @otp ||= {}
46
- @otp[name] ||= begin
47
- secret = @scoped_secret_store[name]
48
- totp = nil
49
- if secret
50
- totp = ROTP::TOTP.new(secret)
51
- else
52
- logger.debug "OTP secret does not exist for '#{name}'"
53
- end
54
- totp
55
- end
56
- end
57
-
58
- end
59
-
60
- end
61
-
@@ -1,19 +0,0 @@
1
- require_relative '../atmos'
2
-
3
- module Atmos
4
- class ProviderFactory
5
- include GemLogger::LoggerSupport
6
-
7
- def self.get(name)
8
- @provider ||= begin
9
- logger.debug("Loading provider: #{name}")
10
- require "atmos/providers/#{name}/provider"
11
- provider = "Atmos::Providers::#{name.camelize}::Provider".constantize
12
- logger.debug("Loaded provider #{provider}")
13
- provider.new(name)
14
- end
15
- return @provider
16
- end
17
-
18
- end
19
- end
@@ -1,82 +0,0 @@
1
- require_relative '../../../atmos'
2
- require 'aws-sdk-organizations'
3
-
4
- module Atmos
5
- module Providers
6
- module Aws
7
-
8
- class AccountManager
9
- include GemLogger::LoggerSupport
10
- include FileUtils
11
-
12
- def initialize(provider)
13
- @provider = provider
14
- end
15
-
16
- def create_account(env, name: nil, email: nil)
17
- result = {}
18
- org = ::Aws::Organizations::Client.new
19
- resp = nil
20
- name ||= "Atmos #{env} account"
21
-
22
- begin
23
- logger.info "Looking up organization"
24
- resp = org.describe_organization()
25
- logger.debug "Described organization: #{resp.to_h}"
26
- rescue ::Aws::Organizations::Errors::AWSOrganizationsNotInUseException
27
- logger.info "Organization doesn't exist, creating"
28
- resp = org.create_organization()
29
- logger.debug "Created organization: #{resp.to_h}"
30
- end
31
-
32
- if email.blank?
33
- master_email = resp.organization.master_account_email
34
- email = master_email.sub('@', "+#{env}@")
35
- end
36
- result[:email] = email
37
- result[:name] = name
38
-
39
-
40
- begin
41
- logger.info "Creating account named #{name}"
42
- resp = org.create_account(account_name: name, email: email)
43
- rescue ::Aws::Organizations::Errors::FinalizingOrganizationException
44
- logger.info "Waiting to retry account creation as the organization needs to finalize"
45
- logger.info "This will eventually succeed after receiving a"
46
- logger.info "'Consolidated Billing verification' email from AWS"
47
- logger.info "You can leave this running or cancel and restart later."
48
- sleep 60
49
- retry
50
- end
51
-
52
- logger.debug "Created account: #{resp.to_h}"
53
-
54
- status_id = resp.create_account_status.id
55
- status = resp.create_account_status.state
56
- account_id = resp.create_account_status.account_id
57
-
58
- while status =~ /in_progress/i
59
- logger.info "Waiting for account creation to complete, status: #{status}"
60
- resp = org.describe_create_account_status(create_account_request_id: status_id)
61
- logger.debug("Account creation status check: #{resp.to_h}")
62
- status = resp.create_account_status.state
63
- account_id = resp.create_account_status.account_id
64
- sleep 5
65
- end
66
-
67
- if status =~ /failed/i
68
- logger.error "Failed to create account: #{resp.create_account_status.failure_reason}"
69
- exit(1)
70
- end
71
-
72
- result[:account_id] = account_id
73
-
74
- return result
75
- end
76
-
77
- end
78
-
79
- end
80
- end
81
- end
82
-
@@ -1,208 +0,0 @@
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 Atmos
10
- module Providers
11
- module Aws
12
-
13
- class AuthManager
14
- include GemLogger::LoggerSupport
15
- include FileUtils
16
- include Atmos::UI
17
-
18
- def initialize(provider)
19
- @provider = provider
20
- end
21
-
22
- def authenticate(system_env, **opts, &block)
23
-
24
- profile = system_env['AWS_PROFILE']
25
- key = system_env['AWS_ACCESS_KEY_ID']
26
- secret = system_env['AWS_SECRET_ACCESS_KEY']
27
- if profile.blank? && (key.blank? || secret.blank?)
28
- logger.warn("An aws profile or key/secret should be supplied via the environment")
29
- end
30
-
31
- # Handle bootstrapping a new env account. Newly created organization
32
- # accounts only have the default role that can only be assumed by an
33
- # iam user, so use that as the target for assume_role, and the root
34
- # check below will ensure iam user
35
- assume_role_name = nil
36
- if opts[:bootstrap] && Atmos.config.atmos_env != 'ops'
37
- # TODO: do this hack better
38
- assume_role_name = Atmos.config["auth.bootstrap_assume_role_name"]
39
- else
40
- assume_role_name = opts[:role] || Atmos.config["auth.assume_role_name"]
41
- end
42
- account_id = Atmos.config.account_hash[Atmos.config.atmos_env].to_s
43
- role_arn = "arn:aws:iam::#{account_id}:role/#{assume_role_name}"
44
-
45
- user_name = nil
46
- begin
47
- sts = ::Aws::STS::Client.new
48
- resp = sts.get_caller_identity
49
- arn_pieces = resp.arn.split(":")
50
- user_name = arn_pieces.last.split("/").last
51
-
52
- # root credentials can't assume role, but they should have full
53
- # access for the current account, so proceed (e.g. for bootstrap).
54
- if arn_pieces.last == "root"
55
-
56
- # We check the account of the caller to prevent root user of ops
57
- # account from bootstrapping an env account, but still allow a
58
- # root user of the env account itself to be able to bootstrap
59
- # (i.e. to allow not organizational accounts to bootstrap using
60
- # their root user)
61
- if arn_pieces[-2] != account_id
62
- logger.error <<~EOF
63
- Account doesn't match credentials. Bootstrapping a new
64
- account should be done as an iam user from the ops account or
65
- using credentials for a root user of the env account.
66
- EOF
67
- exit(1)
68
- end
69
-
70
- # Should only use root credentials for bootstrap, and thus we
71
- # won't have role requirement for mfa, etc, even if root account
72
- # uses mfa for login. Thus skip all the other stuff, to
73
- # encourage/force use of non-root accounts for normal use
74
- logger.warn("Using aws root credentials - should only be neccessary for bootstrap")
75
- return block.call(Hash[system_env])
76
- end
77
-
78
- rescue ::Aws::STS::Errors::ServiceError => e
79
- logger.error "Could not discover aws credentials"
80
- exit(1)
81
- end
82
-
83
- auth_needed = true
84
- cache_key = "#{user_name}-#{assume_role_name}"
85
- credentials = read_auth_cache[cache_key]
86
-
87
- if credentials.present?
88
- logger.debug("Session cache present, checking expiration...")
89
- expiration = Time.parse(credentials['expiration'])
90
- session_renew_interval = (session_duration / 4).to_i
91
-
92
- if Time.now > expiration
93
- logger.debug "Session cache is expired, performing normal auth"
94
- auth_needed = true
95
- elsif Time.now > (expiration - session_renew_interval)
96
- begin
97
- logger.info "Session approaching expiration, renewing..."
98
- credentials = assume_role(role_arn, credentials: credentials)
99
- auth_needed = false
100
- rescue => e
101
- logger.info "Failed to renew credentials using session cache, reason: #{e.message}"
102
- auth_needed = true
103
- end
104
- else
105
- logger.debug "Session cache is current, skipping auth"
106
- auth_needed = false
107
- end
108
- end
109
-
110
- if auth_needed
111
- begin
112
- logger.info "No active session cache, authenticating..."
113
-
114
- credentials = assume_role(role_arn)
115
- write_auth_cache(cache_key => credentials)
116
-
117
- rescue ::Aws::STS::Errors::AccessDenied => e
118
- if e.message !~ /explicit deny/
119
- logger.debug "Access Denied, reason: #{e.message}"
120
- end
121
-
122
- logger.info "Normal auth failed, checking for mfa"
123
-
124
- iam = ::Aws::IAM::Client.new
125
- response = iam.list_mfa_devices(user_name: user_name)
126
- mfa_serial = response.mfa_devices.first.try(:serial_number)
127
- token = nil
128
- if mfa_serial.present?
129
-
130
- token = Atmos::Otp.instance.generate(user_name)
131
- if token.nil?
132
- token = ask("Enter token to retry with mfa: ")
133
- else
134
- logger.info "Used integrated atmos mfa to generate token"
135
- end
136
-
137
- if token.blank?
138
- logger.error "A MFA token must be supplied"
139
- exit(1)
140
- end
141
-
142
- else
143
- logger.error "MFA is not setup for your account, retry after doing so"
144
- exit(1)
145
- end
146
-
147
- credentials = assume_role(role_arn, serial_number: mfa_serial, token_code: token)
148
- write_auth_cache(cache_key => credentials)
149
-
150
- end
151
- end
152
-
153
- process_env = {}
154
- process_env['AWS_ACCESS_KEY_ID'] = credentials['access_key_id']
155
- process_env['AWS_SECRET_ACCESS_KEY'] = credentials['secret_access_key']
156
- process_env['AWS_SESSION_TOKEN'] = credentials['session_token']
157
- logger.debug("Calling authentication target with env: #{process_env.inspect}")
158
- block.call(Hash[system_env].merge(process_env))
159
- end
160
-
161
- private
162
-
163
- def session_duration
164
- @session_duration ||= (Atmos.config["auth.session_duration"] || 3600).to_i
165
- end
166
-
167
- def assume_role(role_arn, **opts)
168
- # use Aws::AssumeRoleCredentials ?
169
- if opts[:credentials]
170
- c = opts.delete(:credentials)
171
- creds = ::Aws::Credentials.new(
172
- c[:access_key_id], c[:secret_access_key], c[:session_token]
173
- )
174
- client_opts = {credentials: creds}
175
- else
176
- client_opts = {}
177
- end
178
- sts = ::Aws::STS::Client.new(client_opts)
179
- params = {
180
- duration_seconds: session_duration,
181
- role_session_name: "Atmos",
182
- role_arn: role_arn
183
- }.merge(opts)
184
- logger.debug("Assuming role: #{params}")
185
- resp = sts.assume_role(params)
186
- return Atmos::Utils::SymbolizedMash.new(resp.credentials.to_h)
187
- end
188
-
189
- def auth_cache_file
190
- File.join(Atmos.config.auth_cache_dir, 'aws-assume-role.json')
191
- end
192
-
193
- def write_auth_cache(h)
194
- File.open(auth_cache_file, 'w') do |f|
195
- f.puts(JSON.pretty_generate(h))
196
- end
197
- end
198
-
199
- def read_auth_cache
200
- data = JSON.parse(File.read(auth_cache_file)) rescue {}
201
- Atmos::Utils::SymbolizedMash.new(data)
202
- end
203
-
204
- end
205
-
206
- end
207
- end
208
- end