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,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
+