simplygenius-atmos 0.7.1 → 0.8.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|