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,93 @@
1
+ require_relative '../atmos'
2
+ require_relative '../atmos/generator'
3
+ require 'tmpdir'
4
+ require 'fileutils'
5
+ require 'git'
6
+ require 'open-uri'
7
+ require 'zip'
8
+
9
+ module Atmos
10
+ class GeneratorFactory
11
+ include GemLogger::LoggerSupport
12
+
13
+ def self.create(sourcepaths, **opts)
14
+ expanded_sourcepaths = expand_sourcepaths(sourcepaths)
15
+ klass = Class.new(Atmos::Generator) do
16
+ source_paths.concat(expanded_sourcepaths)
17
+ end
18
+
19
+ g = klass.new([], **opts)
20
+ return g
21
+ end
22
+
23
+ def self.expand_sourcepaths(sourcepaths)
24
+ expanded_sourcepaths = []
25
+ sourcepaths.each do |sourcepath|
26
+
27
+ if sourcepath =~ /(\.git)|(\.zip)(#.*)?$/
28
+
29
+ logger.debug("Using archive sourcepath")
30
+
31
+ tmpdir = Dir.mktmpdir("atmos-templates-")
32
+ at_exit { FileUtils.remove_entry(tmpdir) }
33
+
34
+ template_subdir = ''
35
+ if sourcepath =~ /([^#]*)#([^#]*)/
36
+ sourcepath = Regexp.last_match[1]
37
+ template_subdir = Regexp.last_match[2]
38
+ logger.debug("Using archive subdirectory for templates: #{template_subdir}")
39
+ end
40
+
41
+ if sourcepath =~ /.git$/
42
+
43
+ begin
44
+ logger.debug("Cloning git archive to tmpdir")
45
+
46
+ g = Git.clone(sourcepath, 'atmos-checkout', depth: 1, path: tmpdir)
47
+ local_template_path = File.join(g.dir.path, template_subdir)
48
+
49
+ expanded_sourcepaths << local_template_path
50
+ logger.debug("Using git sourcepath: #{local_template_path}")
51
+ rescue => e
52
+ logger.log_exception(e, level: :debug)
53
+ logger.warn("Could not read from git archive, ignoring sourcepath: #{sourcepath}")
54
+ end
55
+
56
+ elsif sourcepath =~ /.zip$/
57
+
58
+ begin
59
+ logger.debug("Cloning zip archive to tmpdir")
60
+
61
+ open(sourcepath, 'rb') do |io|
62
+ Zip::File.open_buffer(io) do |zip_file|
63
+ zip_file.each do |f|
64
+ fpath = File.join(tmpdir, f.name)
65
+ f.extract(fpath)
66
+ end
67
+ end
68
+ end
69
+
70
+ local_template_path = File.join(tmpdir, template_subdir)
71
+ expanded_sourcepaths << local_template_path
72
+ logger.debug("Using zip sourcepath: #{local_template_path}")
73
+ rescue => e
74
+ logger.log_exception(e, level: :debug)
75
+ logger.warn("Could not read from zip archive, ignoring sourcepath: #{sourcepath}")
76
+ end
77
+
78
+ end
79
+
80
+ else
81
+
82
+ logger.debug("Using local sourcepath: #{sourcepath}")
83
+ expanded_sourcepaths << sourcepath
84
+
85
+ end
86
+
87
+ end
88
+
89
+ return expanded_sourcepaths
90
+ end
91
+
92
+ end
93
+ end
@@ -0,0 +1,132 @@
1
+ require_relative '../atmos'
2
+ require 'fileutils'
3
+ require 'hashie'
4
+
5
+ module Atmos
6
+ class Ipc
7
+ include GemLogger::LoggerSupport
8
+
9
+ def initialize(sock_dir=Dir.tmpdir)
10
+ @sock_dir = sock_dir
11
+ end
12
+
13
+ def listen(&block)
14
+ raise "Already listening" if @server
15
+
16
+ begin
17
+ @socket_path = File.join(@sock_dir, 'atmos-ipc')
18
+ FileUtils.rm_f(@socket_path)
19
+ @server = UNIXServer.open(@socket_path)
20
+ rescue ArgumentError => e
21
+ if e.message =~ /too long unix socket path/ && @sock_dir != Dir.tmpdir
22
+ logger.warn("Using tmp for ipc socket as path too long: #{@socket_path}")
23
+ @sock_dir = Dir.tmpdir
24
+ retry
25
+ end
26
+ end
27
+
28
+ begin
29
+ thread = Thread.new { run }
30
+ block.call(@socket_path)
31
+ ensure
32
+ @server.close
33
+ FileUtils.rm_f(@socket_path)
34
+ @server = nil
35
+ end
36
+ end
37
+
38
+ def generate_client_script
39
+ script_file = File.join(@sock_dir, 'atmos_ipc.rb')
40
+ File.write(script_file, <<~EOF
41
+ #!/usr/bin/env ruby
42
+ require 'socket'
43
+ UNIXSocket.open('#{@socket_path}') {|c| c.puts(ARGV[0] || $stdin.read); puts c.gets }
44
+ EOF
45
+ )
46
+ FileUtils.chmod('+x', script_file)
47
+ return script_file
48
+ end
49
+
50
+ private
51
+
52
+ def run
53
+ logger.debug("Starting ipc thread")
54
+ begin
55
+ while @server && sock = @server.accept
56
+ logger.debug("An ipc client connected")
57
+ line = sock.gets
58
+ logger.debug("Got ipc message: #{line.inspect}")
59
+ response = {}
60
+
61
+ begin
62
+ msg = JSON.parse(line)
63
+ msg = Hashie.symbolize_keys(msg)
64
+
65
+ # enabled by default if enabled is not set (e.g. from provisioner local-exec)
66
+ enabled = msg[:enabled].nil? ? true : ["true", "1"].include?(msg[:enabled].to_s)
67
+
68
+ if enabled
69
+ logger.debug("Dispatching IPC action")
70
+ response = dispatch(msg)
71
+ else
72
+ response[:message] = "IPC action is not enabled"
73
+ logger.debug(response[:error])
74
+ end
75
+ rescue => e
76
+ logger.log_exception(e, "Failed to parse ipc message")
77
+ response[:error] = "Failed to parse ipc message #{e.message}"
78
+ end
79
+
80
+ respond(sock, response)
81
+ sock.close
82
+ end
83
+ rescue IOError, EOFError, Errno::EBADF
84
+ nil
85
+ rescue Exception => e
86
+ logger.log_exception(e, "Ipc failure")
87
+ end
88
+ end
89
+
90
+ def close
91
+ @server.close if @server rescue nil
92
+ end
93
+
94
+ def load_action(name)
95
+ action = nil
96
+ logger.debug("Loading ipc action: #{name}")
97
+ begin
98
+ require "atmos/ipc_actions/#{name}"
99
+ action = "Atmos::IpcActions::#{name.camelize}".constantize
100
+ logger.debug("Loaded ipc action #{name}")
101
+ rescue LoadError, NameError => e
102
+ logger.log_exception(e, "Failed to load ipc action")
103
+ end
104
+ return action
105
+ end
106
+
107
+ def dispatch(msg)
108
+ response = {}
109
+ action = load_action(msg[:action])
110
+ if action.nil?
111
+ response[:error] = "Unsupported ipc action: #{msg.to_hash.inspect}"
112
+ logger.warn(response[:error])
113
+ else
114
+ begin
115
+ response = action.new().execute(**msg)
116
+ rescue => e
117
+ response[:error] = "Failure while executing ipc action: #{e.message}"
118
+ logger.log_exception(e, "Failure while executing ipc action")
119
+ end
120
+ end
121
+ return response
122
+ end
123
+
124
+ def respond(sock, response)
125
+ msg = JSON.generate(response)
126
+ logger.debug("Sending ipc response: #{msg.inspect}")
127
+ sock.puts(msg)
128
+ sock.flush
129
+ end
130
+
131
+ end
132
+ end
@@ -0,0 +1,27 @@
1
+ require_relative '../../atmos'
2
+ require_relative '../../atmos/ui'
3
+
4
+ module Atmos
5
+ module IpcActions
6
+ class Notify
7
+ include GemLogger::LoggerSupport
8
+ include Atmos::UI
9
+
10
+ def initialize()
11
+ end
12
+
13
+ def execute(**opts)
14
+
15
+ result = {
16
+ 'stdout' => '',
17
+ 'success' => ''
18
+ }
19
+
20
+ return result if Atmos.config["ipc.notify.disable"].to_s == "true"
21
+ return notify(**opts)
22
+
23
+ end
24
+
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,19 @@
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
@@ -0,0 +1,160 @@
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
@@ -0,0 +1,61 @@
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
+