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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +2 -0
- data/LICENSE +13 -0
- data/README.md +212 -0
- data/exe/atmos +4 -0
- data/exe/atmos-docker +12 -0
- data/lib/atmos.rb +12 -0
- data/lib/atmos/cli.rb +105 -0
- data/lib/atmos/commands/account.rb +65 -0
- data/lib/atmos/commands/apply.rb +20 -0
- data/lib/atmos/commands/auth_exec.rb +29 -0
- data/lib/atmos/commands/base_command.rb +12 -0
- data/lib/atmos/commands/bootstrap.rb +72 -0
- data/lib/atmos/commands/container.rb +58 -0
- data/lib/atmos/commands/destroy.rb +18 -0
- data/lib/atmos/commands/generate.rb +90 -0
- data/lib/atmos/commands/init.rb +18 -0
- data/lib/atmos/commands/new.rb +18 -0
- data/lib/atmos/commands/otp.rb +54 -0
- data/lib/atmos/commands/plan.rb +20 -0
- data/lib/atmos/commands/secret.rb +87 -0
- data/lib/atmos/commands/terraform.rb +52 -0
- data/lib/atmos/commands/user.rb +74 -0
- data/lib/atmos/config.rb +208 -0
- data/lib/atmos/exceptions.rb +9 -0
- data/lib/atmos/generator.rb +199 -0
- data/lib/atmos/generator_factory.rb +93 -0
- data/lib/atmos/ipc.rb +132 -0
- data/lib/atmos/ipc_actions/notify.rb +27 -0
- data/lib/atmos/ipc_actions/ping.rb +19 -0
- data/lib/atmos/logging.rb +160 -0
- data/lib/atmos/otp.rb +61 -0
- data/lib/atmos/provider_factory.rb +19 -0
- data/lib/atmos/providers/aws/account_manager.rb +82 -0
- data/lib/atmos/providers/aws/auth_manager.rb +208 -0
- data/lib/atmos/providers/aws/container_manager.rb +116 -0
- data/lib/atmos/providers/aws/provider.rb +51 -0
- data/lib/atmos/providers/aws/s3_secret_manager.rb +49 -0
- data/lib/atmos/providers/aws/user_manager.rb +211 -0
- data/lib/atmos/settings_hash.rb +90 -0
- data/lib/atmos/terraform_executor.rb +267 -0
- data/lib/atmos/ui.rb +159 -0
- data/lib/atmos/utils.rb +50 -0
- data/lib/atmos/version.rb +3 -0
- data/templates/new/config/atmos.yml +50 -0
- data/templates/new/config/atmos/runtime.yml +43 -0
- data/templates/new/templates.yml +1 -0
- 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
|
data/lib/atmos/ipc.rb
ADDED
@@ -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
|
data/lib/atmos/otp.rb
ADDED
@@ -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
|
+
|