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.
- checksums.yaml +4 -4
- data/README.md +4 -4
- data/exe/atmos +2 -2
- data/lib/{atmos.rb → simplygenius/atmos.rb} +9 -7
- data/lib/simplygenius/atmos/cli.rb +116 -0
- data/lib/simplygenius/atmos/commands/account.rb +69 -0
- data/lib/simplygenius/atmos/commands/apply.rb +24 -0
- data/lib/simplygenius/atmos/commands/auth_exec.rb +34 -0
- data/lib/simplygenius/atmos/commands/base_command.rb +16 -0
- data/lib/simplygenius/atmos/commands/bootstrap.rb +76 -0
- data/lib/simplygenius/atmos/commands/container.rb +62 -0
- data/lib/simplygenius/atmos/commands/destroy.rb +22 -0
- data/lib/simplygenius/atmos/commands/generate.rb +187 -0
- data/lib/simplygenius/atmos/commands/init.rb +22 -0
- data/lib/simplygenius/atmos/commands/new.rb +22 -0
- data/lib/simplygenius/atmos/commands/otp.rb +58 -0
- data/lib/simplygenius/atmos/commands/plan.rb +24 -0
- data/lib/simplygenius/atmos/commands/secret.rb +91 -0
- data/lib/simplygenius/atmos/commands/terraform.rb +56 -0
- data/lib/simplygenius/atmos/commands/user.rb +78 -0
- data/lib/simplygenius/atmos/config.rb +279 -0
- data/lib/simplygenius/atmos/exceptions.rb +13 -0
- data/lib/simplygenius/atmos/generator.rb +232 -0
- data/lib/simplygenius/atmos/ipc.rb +136 -0
- data/lib/simplygenius/atmos/ipc_actions/notify.rb +31 -0
- data/lib/simplygenius/atmos/ipc_actions/ping.rb +23 -0
- data/lib/simplygenius/atmos/logging.rb +164 -0
- data/lib/simplygenius/atmos/otp.rb +62 -0
- data/lib/simplygenius/atmos/plugin.rb +27 -0
- data/lib/simplygenius/atmos/plugin_manager.rb +120 -0
- data/lib/simplygenius/atmos/plugins/output_filter.rb +29 -0
- data/lib/simplygenius/atmos/plugins/prompt_notify.rb +21 -0
- data/lib/simplygenius/atmos/provider_factory.rb +23 -0
- data/lib/simplygenius/atmos/providers/aws/account_manager.rb +83 -0
- data/lib/simplygenius/atmos/providers/aws/auth_manager.rb +220 -0
- data/lib/simplygenius/atmos/providers/aws/container_manager.rb +118 -0
- data/lib/simplygenius/atmos/providers/aws/provider.rb +53 -0
- data/lib/simplygenius/atmos/providers/aws/s3_secret_manager.rb +51 -0
- data/lib/simplygenius/atmos/providers/aws/user_manager.rb +213 -0
- data/lib/simplygenius/atmos/settings_hash.rb +93 -0
- data/lib/simplygenius/atmos/source_path.rb +186 -0
- data/lib/simplygenius/atmos/template.rb +117 -0
- data/lib/simplygenius/atmos/terraform_executor.rb +297 -0
- data/lib/simplygenius/atmos/ui.rb +173 -0
- data/lib/simplygenius/atmos/utils.rb +54 -0
- data/lib/simplygenius/atmos/version.rb +5 -0
- data/templates/new/config/atmos.yml +21 -13
- data/templates/new/config/atmos/recipes.yml +16 -0
- data/templates/new/config/atmos/runtime.yml +9 -0
- metadata +46 -40
- data/lib/atmos/cli.rb +0 -105
- data/lib/atmos/commands/account.rb +0 -65
- data/lib/atmos/commands/apply.rb +0 -20
- data/lib/atmos/commands/auth_exec.rb +0 -29
- data/lib/atmos/commands/base_command.rb +0 -12
- data/lib/atmos/commands/bootstrap.rb +0 -72
- data/lib/atmos/commands/container.rb +0 -58
- data/lib/atmos/commands/destroy.rb +0 -18
- data/lib/atmos/commands/generate.rb +0 -90
- data/lib/atmos/commands/init.rb +0 -18
- data/lib/atmos/commands/new.rb +0 -18
- data/lib/atmos/commands/otp.rb +0 -54
- data/lib/atmos/commands/plan.rb +0 -20
- data/lib/atmos/commands/secret.rb +0 -87
- data/lib/atmos/commands/terraform.rb +0 -52
- data/lib/atmos/commands/user.rb +0 -74
- data/lib/atmos/config.rb +0 -208
- data/lib/atmos/exceptions.rb +0 -9
- data/lib/atmos/generator.rb +0 -199
- data/lib/atmos/generator_factory.rb +0 -93
- data/lib/atmos/ipc.rb +0 -132
- data/lib/atmos/ipc_actions/notify.rb +0 -27
- data/lib/atmos/ipc_actions/ping.rb +0 -19
- data/lib/atmos/logging.rb +0 -160
- data/lib/atmos/otp.rb +0 -61
- data/lib/atmos/provider_factory.rb +0 -19
- data/lib/atmos/providers/aws/account_manager.rb +0 -82
- data/lib/atmos/providers/aws/auth_manager.rb +0 -208
- data/lib/atmos/providers/aws/container_manager.rb +0 -116
- data/lib/atmos/providers/aws/provider.rb +0 -51
- data/lib/atmos/providers/aws/s3_secret_manager.rb +0 -49
- data/lib/atmos/providers/aws/user_manager.rb +0 -211
- data/lib/atmos/settings_hash.rb +0 -90
- data/lib/atmos/terraform_executor.rb +0 -267
- data/lib/atmos/ui.rb +0 -159
- data/lib/atmos/utils.rb +0 -50
- data/lib/atmos/version.rb +0 -3
@@ -0,0 +1,186 @@
|
|
1
|
+
require_relative '../atmos'
|
2
|
+
require_relative '../atmos/ui'
|
3
|
+
require_relative '../atmos/template'
|
4
|
+
require 'find'
|
5
|
+
require 'tmpdir'
|
6
|
+
require 'fileutils'
|
7
|
+
require 'git'
|
8
|
+
require 'open-uri'
|
9
|
+
require 'zip'
|
10
|
+
|
11
|
+
module SimplyGenius
|
12
|
+
module Atmos
|
13
|
+
|
14
|
+
class SourcePath
|
15
|
+
include GemLogger::LoggerSupport
|
16
|
+
|
17
|
+
class_attribute :registry, default: {}
|
18
|
+
attr_reader :name, :location
|
19
|
+
|
20
|
+
def self.clear_registry
|
21
|
+
registry.clear
|
22
|
+
@resolved_templates.clear if @resolved_templates
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.register(name, location)
|
26
|
+
sp = SourcePath.new(name, location)
|
27
|
+
raise ArgumentError.new("Source paths must be uniquely named: #{sp}") if registry[name]
|
28
|
+
registry[name] = sp
|
29
|
+
end
|
30
|
+
|
31
|
+
def self.find_template(template_name)
|
32
|
+
@resolved_templates ||= {}
|
33
|
+
@resolved_templates[template_name] ||= begin
|
34
|
+
tmpls = registry.collect {|name, sp| sp.template(template_name) }.compact
|
35
|
+
|
36
|
+
if tmpls.size == 0
|
37
|
+
raise ArgumentError.new("Could not find the template: #{template_name}")
|
38
|
+
elsif tmpls.size > 1
|
39
|
+
raise ArgumentError.new("Template names must be unique, #{template_name} exists in multiple sources: #{tmpls.collect(&:source)}")
|
40
|
+
end
|
41
|
+
|
42
|
+
tmpls.first
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def initialize(name, location)
|
47
|
+
@name = name
|
48
|
+
@location = location
|
49
|
+
end
|
50
|
+
|
51
|
+
def to_s
|
52
|
+
"#{name} (#{location})"
|
53
|
+
end
|
54
|
+
|
55
|
+
def to_h
|
56
|
+
SettingsHash.new({name: name, location: location})
|
57
|
+
end
|
58
|
+
|
59
|
+
def directory
|
60
|
+
if @directory_resolved
|
61
|
+
@directory
|
62
|
+
else
|
63
|
+
@directory_resolved = true
|
64
|
+
@directory = expand_location
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def template_names
|
69
|
+
templates.keys.sort
|
70
|
+
end
|
71
|
+
|
72
|
+
def template(name)
|
73
|
+
templates[name]
|
74
|
+
end
|
75
|
+
|
76
|
+
protected
|
77
|
+
|
78
|
+
def expand_location
|
79
|
+
sourcepath_dir = nil
|
80
|
+
sourcepath = location
|
81
|
+
if sourcepath =~ /(\.git)|(\.zip)(#.*)?$/
|
82
|
+
|
83
|
+
logger.debug("Using archive sourcepath")
|
84
|
+
|
85
|
+
tmpdir = Dir.mktmpdir("atmos-templates-")
|
86
|
+
at_exit { FileUtils.remove_entry(tmpdir) }
|
87
|
+
|
88
|
+
template_subdir = ''
|
89
|
+
if sourcepath =~ /([^#]*)#([^#]*)/
|
90
|
+
sourcepath = Regexp.last_match[1]
|
91
|
+
template_subdir = Regexp.last_match[2]
|
92
|
+
logger.debug("Using archive subdirectory for templates: #{template_subdir}")
|
93
|
+
end
|
94
|
+
|
95
|
+
if sourcepath =~ /.git$/
|
96
|
+
|
97
|
+
begin
|
98
|
+
logger.debug("Cloning git archive to tmpdir")
|
99
|
+
|
100
|
+
g = Git.clone(sourcepath, 'atmos-checkout', depth: 1, path: tmpdir)
|
101
|
+
local_template_path = File.join(g.dir.path, template_subdir)
|
102
|
+
|
103
|
+
sourcepath_dir = File.expand_path(local_template_path)
|
104
|
+
logger.debug("Using git sourcepath: #{sourcepath_dir}")
|
105
|
+
rescue => e
|
106
|
+
msg = "Could not read from git archive, ignoring sourcepath: #{name}, #{location}"
|
107
|
+
logger.log_exception(e, msg, level: :debug)
|
108
|
+
logger.warn(msg)
|
109
|
+
end
|
110
|
+
|
111
|
+
elsif sourcepath =~ /.zip$/
|
112
|
+
|
113
|
+
begin
|
114
|
+
logger.debug("Cloning zip archive to tmpdir")
|
115
|
+
|
116
|
+
open(sourcepath, 'rb') do |io|
|
117
|
+
Zip::File.open_buffer(io) do |zip_file|
|
118
|
+
zip_file.each do |f|
|
119
|
+
fpath = File.join(tmpdir, f.name)
|
120
|
+
f.extract(fpath)
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
local_template_path = File.join(tmpdir, template_subdir)
|
126
|
+
sourcepath_dir = File.expand_path(local_template_path)
|
127
|
+
logger.debug("Using zip sourcepath: #{sourcepath_dir}")
|
128
|
+
rescue => e
|
129
|
+
msg = "Could not read from zip archive, ignoring sourcepath: #{name}, #{location}"
|
130
|
+
logger.log_exception(e, msg, level: :debug)
|
131
|
+
logger.warn(msg)
|
132
|
+
end
|
133
|
+
|
134
|
+
end
|
135
|
+
|
136
|
+
else
|
137
|
+
|
138
|
+
sourcepath_dir = File.expand_path(sourcepath)
|
139
|
+
logger.debug("Using local sourcepath: #{sourcepath_dir}")
|
140
|
+
|
141
|
+
end
|
142
|
+
|
143
|
+
sourcepath_dir
|
144
|
+
end
|
145
|
+
|
146
|
+
def template_dirs
|
147
|
+
@template_dirs ||= begin
|
148
|
+
template_dirs = {}
|
149
|
+
if directory && Dir.exist?(directory)
|
150
|
+
|
151
|
+
Find.find(directory) do |f|
|
152
|
+
Find.prune if File.basename(f) =~ /(^\.)|svn|CVS|git/
|
153
|
+
|
154
|
+
template_spec = File.join(f, Template::TEMPLATES_SPEC_FILE)
|
155
|
+
if File.exist?(template_spec)
|
156
|
+
template_name = f.sub(/^#{directory}\//, '')
|
157
|
+
|
158
|
+
if template_dirs[template_name]
|
159
|
+
# safety, this should never get hit
|
160
|
+
raise "A single source path cannot have duplicate templates: #{f}"
|
161
|
+
end
|
162
|
+
template_dirs[template_name] = f
|
163
|
+
Find.prune
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
else
|
168
|
+
|
169
|
+
logger.warn("Sourcepath directory does not exist for location: #{location}, #{directory}")
|
170
|
+
|
171
|
+
end
|
172
|
+
|
173
|
+
template_dirs
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
def templates
|
178
|
+
@templates ||= Hash[template_dirs.collect do |tname, dir|
|
179
|
+
[tname, Template.new(tname, dir, self)]
|
180
|
+
end]
|
181
|
+
end
|
182
|
+
|
183
|
+
end
|
184
|
+
|
185
|
+
end
|
186
|
+
end
|
@@ -0,0 +1,117 @@
|
|
1
|
+
require_relative '../atmos'
|
2
|
+
require_relative '../atmos/source_path'
|
3
|
+
|
4
|
+
module SimplyGenius
|
5
|
+
module Atmos
|
6
|
+
|
7
|
+
class Template
|
8
|
+
include GemLogger::LoggerSupport
|
9
|
+
|
10
|
+
TEMPLATES_SPEC_FILE = 'templates.yml'
|
11
|
+
TEMPLATES_ACTIONS_FILE = 'templates.rb'
|
12
|
+
|
13
|
+
attr_reader :name, :directory, :source, :context
|
14
|
+
|
15
|
+
def initialize(name, directory, source, context: {})
|
16
|
+
@name = name
|
17
|
+
@directory = directory
|
18
|
+
@source = source
|
19
|
+
@context = context
|
20
|
+
@context = SettingsHash.new(@context) unless @context.kind_of?(SettingsHash)
|
21
|
+
end
|
22
|
+
|
23
|
+
def to_s
|
24
|
+
"#{name}"
|
25
|
+
end
|
26
|
+
|
27
|
+
def to_h
|
28
|
+
SettingsHash.new({name: name, source: source.to_h, context: context})
|
29
|
+
end
|
30
|
+
|
31
|
+
def context_path
|
32
|
+
name.gsub('-', '_').gsub('/', '.')
|
33
|
+
end
|
34
|
+
|
35
|
+
def scoped_context
|
36
|
+
result = context.notation_get(context_path)
|
37
|
+
if result.nil?
|
38
|
+
context.notation_put(context_path, SettingsHash.new, additive: false)
|
39
|
+
result = context.notation_get(context_path)
|
40
|
+
end
|
41
|
+
result
|
42
|
+
end
|
43
|
+
|
44
|
+
def actions_path
|
45
|
+
File.join(directory, TEMPLATES_ACTIONS_FILE)
|
46
|
+
end
|
47
|
+
|
48
|
+
def actions
|
49
|
+
@actions ||= (File.exist?(actions_path) ? File.read(actions_path) : "")
|
50
|
+
end
|
51
|
+
|
52
|
+
def config_path
|
53
|
+
File.join(directory, TEMPLATES_SPEC_FILE)
|
54
|
+
end
|
55
|
+
|
56
|
+
def config
|
57
|
+
@config ||= begin
|
58
|
+
data = File.read(config_path)
|
59
|
+
SettingsHash.new(YAML.load(data) || {})
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def optional
|
64
|
+
result = config[:optional] || {}
|
65
|
+
raise TypeError.new("Template config item :optional must be a hash: #{result.inspect}") unless result.is_a?(Hash)
|
66
|
+
result
|
67
|
+
end
|
68
|
+
|
69
|
+
def dependencies
|
70
|
+
@dependencies ||= begin
|
71
|
+
deps = Array(config[:dependent_templates])
|
72
|
+
deps.collect do |d|
|
73
|
+
if d.kind_of?(String)
|
74
|
+
tmpl = SourcePath.find_template(d)
|
75
|
+
elsif d.kind_of?(Hash)
|
76
|
+
raise ArgumentError.new("Template must be named with name key: #{tmpl}") unless d[:name]
|
77
|
+
tmpl = SourcePath.find_template(d[:name])
|
78
|
+
tmpl.context.merge!(d[:context]) if d[:context]
|
79
|
+
else
|
80
|
+
raise TypeError.new("Invalid template structure: #{d}")
|
81
|
+
end
|
82
|
+
|
83
|
+
tmpl
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
def dup
|
89
|
+
dependencies
|
90
|
+
Marshal.load(Marshal.dump(self))
|
91
|
+
end
|
92
|
+
|
93
|
+
# depth first iteration of dependencies
|
94
|
+
def walk_dependencies(seen=Set.new)
|
95
|
+
Enumerator.new do |yielder|
|
96
|
+
if seen.include?(name)
|
97
|
+
seen << name
|
98
|
+
raise ArgumentError.new("Circular template dependency: #{seen.to_a.join(" => ")}")
|
99
|
+
end
|
100
|
+
seen << name
|
101
|
+
|
102
|
+
dependencies.each do |dep|
|
103
|
+
|
104
|
+
dep = dep.dup
|
105
|
+
dep.context.merge!(context)
|
106
|
+
dep.walk_dependencies(seen.dup).each do |d|
|
107
|
+
yielder << d
|
108
|
+
end
|
109
|
+
end
|
110
|
+
yielder << dup
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
end
|
115
|
+
|
116
|
+
end
|
117
|
+
end
|
@@ -0,0 +1,297 @@
|
|
1
|
+
require_relative '../atmos'
|
2
|
+
require_relative '../atmos/ipc'
|
3
|
+
require_relative '../atmos/ui'
|
4
|
+
require 'open3'
|
5
|
+
require 'fileutils'
|
6
|
+
require 'find'
|
7
|
+
require 'climate_control'
|
8
|
+
|
9
|
+
module SimplyGenius
|
10
|
+
module Atmos
|
11
|
+
|
12
|
+
class TerraformExecutor
|
13
|
+
include GemLogger::LoggerSupport
|
14
|
+
include FileUtils
|
15
|
+
include UI
|
16
|
+
|
17
|
+
class ProcessFailed < RuntimeError; end
|
18
|
+
|
19
|
+
def initialize(process_env: ENV, working_group: 'default')
|
20
|
+
@process_env = process_env
|
21
|
+
@working_group = working_group
|
22
|
+
@working_dir = Atmos.config.tf_working_dir(@working_group)
|
23
|
+
@recipes = Atmos.config["recipes.#{@working_group}"]
|
24
|
+
end
|
25
|
+
|
26
|
+
def run(*terraform_args, skip_backend: false, skip_secrets: false, get_modules: false, output_io: nil)
|
27
|
+
setup_working_dir(skip_backend: skip_backend)
|
28
|
+
|
29
|
+
if get_modules
|
30
|
+
logger.debug("Getting modules")
|
31
|
+
get_modules_io = StringIO.new
|
32
|
+
begin
|
33
|
+
execute("get", output_io: get_modules_io)
|
34
|
+
rescue TerraformExecutor::ProcessFailed => e
|
35
|
+
logger.info(get_modules_io.string)
|
36
|
+
raise
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
return execute(*terraform_args, skip_secrets: skip_secrets, output_io: output_io)
|
41
|
+
end
|
42
|
+
|
43
|
+
private
|
44
|
+
|
45
|
+
def tf_cmd(*args)
|
46
|
+
['terraform'] + args
|
47
|
+
end
|
48
|
+
|
49
|
+
def execute(*terraform_args, skip_secrets: false, output_io: nil)
|
50
|
+
cmd = tf_cmd(*terraform_args)
|
51
|
+
logger.debug("Running terraform: #{cmd.join(' ')}")
|
52
|
+
|
53
|
+
env = Hash[@process_env]
|
54
|
+
if ! skip_secrets
|
55
|
+
begin
|
56
|
+
env = env.merge(secrets_env)
|
57
|
+
rescue => e
|
58
|
+
logger.debug("Secrets not available: #{e}")
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
# lets tempfiles created by subprocesses be easily found by users
|
63
|
+
env['TMPDIR'] = Atmos.config.tmp_dir
|
64
|
+
|
65
|
+
# Lets terraform communicate back to atmos, e.g. for UI notifications
|
66
|
+
ipc = Ipc.new(Atmos.config.tmp_dir)
|
67
|
+
|
68
|
+
IO.pipe do |stdout, stdout_writer|
|
69
|
+
IO.pipe do |stderr, stderr_writer|
|
70
|
+
|
71
|
+
stdout_writer.sync = stderr_writer.sync = true
|
72
|
+
|
73
|
+
stdout_filters = Atmos.config.plugin_manager.output_filters(:stdout, {process_env: @process_env, working_group: @working_group})
|
74
|
+
stderr_filters = Atmos.config.plugin_manager.output_filters(:stderr, {process_env: @process_env, working_group: @working_group})
|
75
|
+
|
76
|
+
stdout_thr = pipe_stream(stdout, output_io.nil? ? $stdout : output_io, &stdout_filters.filter_block)
|
77
|
+
stderr_thr = pipe_stream(stderr, output_io.nil? ? $stderr : output_io, &stderr_filters.filter_block)
|
78
|
+
|
79
|
+
ipc.listen do |sock_path|
|
80
|
+
|
81
|
+
if Atmos.config['ipc.disable']
|
82
|
+
# Using : as the command makes execution of ipc from the
|
83
|
+
# terraform side a no-op in both cases of how we call it. This
|
84
|
+
# way, terraform execution continues to work when IPC is disabled
|
85
|
+
# command = "$ATMOS_IPC_CLIENT <json_string>"
|
86
|
+
# program = ["sh", "-c", "$ATMOS_IPC_CLIENT"]
|
87
|
+
env['ATMOS_IPC_CLIENT'] = ":"
|
88
|
+
else
|
89
|
+
env['ATMOS_IPC_SOCK'] = sock_path
|
90
|
+
env['ATMOS_IPC_CLIENT'] = ipc.generate_client_script
|
91
|
+
end
|
92
|
+
|
93
|
+
# Was unable to get piping to work with stdin for some reason. It
|
94
|
+
# worked in simple case, but started to fail when terraform config
|
95
|
+
# got more extensive. Thus, using spawn to redirect stdin from the
|
96
|
+
# terminal direct to terraform, with IO.pipe to copy the outher
|
97
|
+
# streams. Maybe in the future we can completely disconnect stdin
|
98
|
+
# and have atmos do the output parsing and stdin prompting
|
99
|
+
pid = spawn(env, *cmd,
|
100
|
+
chdir: tf_recipes_dir,
|
101
|
+
:out=>stdout_writer, :err=> stderr_writer, :in => :in)
|
102
|
+
|
103
|
+
logger.debug("Terraform started with pid #{pid}")
|
104
|
+
begin
|
105
|
+
Process.wait(pid)
|
106
|
+
rescue Interrupt
|
107
|
+
logger.warn "Got SIGINT, sending to terraform pid=#{pid}"
|
108
|
+
|
109
|
+
Process.kill("INT", pid)
|
110
|
+
Process.wait(pid)
|
111
|
+
|
112
|
+
logger.debug "Completed signal cleanup"
|
113
|
+
exit!(1)
|
114
|
+
end
|
115
|
+
|
116
|
+
end
|
117
|
+
|
118
|
+
stdout_writer.close
|
119
|
+
stderr_writer.close
|
120
|
+
stdout_thr.join
|
121
|
+
stderr_thr.join
|
122
|
+
stdout_filters.close
|
123
|
+
stderr_filters.close
|
124
|
+
|
125
|
+
status = $?.exitstatus
|
126
|
+
logger.debug("Terraform exited: #{status}")
|
127
|
+
if status != 0
|
128
|
+
raise ProcessFailed.new "Terraform exited with non-zero exit code: #{status}"
|
129
|
+
end
|
130
|
+
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
end
|
135
|
+
|
136
|
+
def setup_working_dir(skip_backend: false)
|
137
|
+
clean_links
|
138
|
+
link_shared_plugin_dir
|
139
|
+
link_support_dirs
|
140
|
+
link_recipes
|
141
|
+
write_atmos_vars
|
142
|
+
setup_backend(skip_backend)
|
143
|
+
end
|
144
|
+
|
145
|
+
def setup_backend(skip_backend=false)
|
146
|
+
backend_file = File.join(tf_recipes_dir, 'atmos-backend.tf.json')
|
147
|
+
backend_config = (Atmos.config["backend"] || {}).clone
|
148
|
+
|
149
|
+
if backend_config.present? && ! skip_backend
|
150
|
+
logger.debug("Writing out terraform state backend config")
|
151
|
+
|
152
|
+
# Use a different state file per group
|
153
|
+
if @working_group
|
154
|
+
backend_config['key'] = "#{@working_group}-#{backend_config['key']}"
|
155
|
+
end
|
156
|
+
|
157
|
+
backend_type = backend_config.delete("type")
|
158
|
+
|
159
|
+
backend = {
|
160
|
+
"terraform" => {
|
161
|
+
"backend" => {
|
162
|
+
backend_type => backend_config
|
163
|
+
}
|
164
|
+
}
|
165
|
+
}
|
166
|
+
|
167
|
+
File.write(backend_file, JSON.pretty_generate(backend))
|
168
|
+
else
|
169
|
+
logger.debug("Clearing terraform state backend config")
|
170
|
+
File.delete(backend_file) if File.exist?(backend_file)
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
# terraform currently (v0.11.7) doesn't handle maps with nested maps or
|
175
|
+
# lists well, so flatten them - nested maps get expanded into the top level
|
176
|
+
# one, with their keys being appended with underscores, and lists get
|
177
|
+
# joined with "," so we end up with a single hash with homogenous types
|
178
|
+
#
|
179
|
+
def homogenize_for_terraform(obj, prefix="")
|
180
|
+
if obj.is_a? Hash
|
181
|
+
result = {}
|
182
|
+
obj.each do |k, v|
|
183
|
+
ho = homogenize_for_terraform(v, "#{prefix}#{k}_")
|
184
|
+
if ho.is_a? Hash
|
185
|
+
result = result.merge(ho)
|
186
|
+
else
|
187
|
+
result["#{prefix}#{k}"] = ho
|
188
|
+
end
|
189
|
+
end
|
190
|
+
return result
|
191
|
+
elsif obj.is_a? Array
|
192
|
+
result = []
|
193
|
+
obj.each do |o|
|
194
|
+
ho = homogenize_for_terraform(o, prefix)
|
195
|
+
if ho.is_a? Hash
|
196
|
+
result << ho.collect {|k, v| "#{k}=#{v}"}.join(";")
|
197
|
+
else
|
198
|
+
result << ho
|
199
|
+
end
|
200
|
+
end
|
201
|
+
return result.join(",")
|
202
|
+
else
|
203
|
+
return obj
|
204
|
+
end
|
205
|
+
end
|
206
|
+
|
207
|
+
def tf_recipes_dir
|
208
|
+
@tf_recipes_dir ||= begin
|
209
|
+
dir = File.join(@working_dir, 'recipes')
|
210
|
+
logger.debug("Tf recipes dir: #{dir}")
|
211
|
+
mkdir_p(dir)
|
212
|
+
dir
|
213
|
+
end
|
214
|
+
end
|
215
|
+
|
216
|
+
def write_atmos_vars
|
217
|
+
File.open(File.join(tf_recipes_dir, 'atmos.auto.tfvars.json'), 'w') do |f|
|
218
|
+
# A mapping in the auto vars file is ignored if a variable declaration doesn't exist for it in a tf file. Thus,
|
219
|
+
# as a convenience to allow everything from atmos to be referenceable, we put everything from the atmos_config
|
220
|
+
# in a homogenized hash named atmos_config which is declared by the atmos scaffolding. For variables which are
|
221
|
+
# declared, we also merge in atmos config with only the values homogenized (vs the entire map) so that hash
|
222
|
+
# variables if declared in terraform can be managed from yml, set here and accessed from terraform
|
223
|
+
#
|
224
|
+
atmos_config = homogenize_for_terraform(Atmos.config.to_h)
|
225
|
+
var_hash = {
|
226
|
+
atmos_env: Atmos.config.atmos_env,
|
227
|
+
all_env_names: Atmos.config.all_env_names,
|
228
|
+
account_ids: Atmos.config.account_hash,
|
229
|
+
atmos_config: atmos_config
|
230
|
+
}
|
231
|
+
var_hash = var_hash.merge(Atmos.config.to_h)
|
232
|
+
f.puts(JSON.pretty_generate(var_hash))
|
233
|
+
end
|
234
|
+
end
|
235
|
+
|
236
|
+
def secrets_env
|
237
|
+
# NOTE use an auto-deleting temp file if passing secrets through env ends
|
238
|
+
# up being problematic
|
239
|
+
# TODO fix the need for CC - TE calls for secrets which needs auth in
|
240
|
+
# ENV, so kinda clunk to have to do both CC and pass the env in
|
241
|
+
ClimateControl.modify(@process_env) do
|
242
|
+
secrets = Atmos.config.provider.secret_manager.to_h
|
243
|
+
env_secrets = Hash[secrets.collect { |k, v| ["TF_VAR_#{k}", v] }]
|
244
|
+
return env_secrets
|
245
|
+
end
|
246
|
+
end
|
247
|
+
|
248
|
+
def clean_links
|
249
|
+
Find.find(@working_dir) do |f|
|
250
|
+
Find.prune if f =~ /\/.terraform\/modules\//
|
251
|
+
File.delete(f) if File.symlink?(f)
|
252
|
+
end
|
253
|
+
end
|
254
|
+
|
255
|
+
def link_support_dirs
|
256
|
+
['modules', 'templates'].each do |subdir|
|
257
|
+
ln_sf(File.join(Atmos.config.root_dir, subdir), @working_dir)
|
258
|
+
end
|
259
|
+
end
|
260
|
+
|
261
|
+
def link_shared_plugin_dir
|
262
|
+
if ! Atmos.config["terraform.disable_shared_plugins"]
|
263
|
+
shared_plugins_dir = File.join(Atmos.config.tmp_root, "terraform_plugins")
|
264
|
+
mkdir_p(shared_plugins_dir)
|
265
|
+
terraform_state_dir = File.join(tf_recipes_dir, '.terraform')
|
266
|
+
mkdir_p(terraform_state_dir)
|
267
|
+
terraform_plugins_dir = File.join(terraform_state_dir, 'plugins')
|
268
|
+
ln_sf(shared_plugins_dir, terraform_plugins_dir)
|
269
|
+
end
|
270
|
+
end
|
271
|
+
|
272
|
+
def link_recipes
|
273
|
+
@recipes.each do |recipe|
|
274
|
+
ln_sf(File.join(Atmos.config.root_dir, 'recipes', "#{recipe}.tf"), tf_recipes_dir)
|
275
|
+
end
|
276
|
+
end
|
277
|
+
|
278
|
+
def pipe_stream(src, dest)
|
279
|
+
Thread.new do
|
280
|
+
block_size = 1024
|
281
|
+
begin
|
282
|
+
while data = src.readpartial(block_size)
|
283
|
+
data = yield data if block_given?
|
284
|
+
dest.write(data)
|
285
|
+
end
|
286
|
+
rescue IOError, EOFError => e
|
287
|
+
logger.log_exception(e, "Stream failure", level: :debug)
|
288
|
+
rescue Exception => e
|
289
|
+
logger.log_exception(e, "Stream failure")
|
290
|
+
end
|
291
|
+
end
|
292
|
+
end
|
293
|
+
|
294
|
+
end
|
295
|
+
|
296
|
+
end
|
297
|
+
end
|