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,49 @@
|
|
1
|
+
require_relative '../../../atmos'
|
2
|
+
require 'aws-sdk-s3'
|
3
|
+
|
4
|
+
module Atmos
|
5
|
+
module Providers
|
6
|
+
module Aws
|
7
|
+
|
8
|
+
class S3SecretManager
|
9
|
+
include GemLogger::LoggerSupport
|
10
|
+
|
11
|
+
def initialize(provider)
|
12
|
+
@provider = provider
|
13
|
+
logger.debug("Secrets config is: #{Atmos.config[:secret]}")
|
14
|
+
@bucket_name = Atmos.config[:secret][:bucket]
|
15
|
+
@encrypt = Atmos.config[:secret][:encrypt]
|
16
|
+
end
|
17
|
+
|
18
|
+
def set(key, value)
|
19
|
+
opts = {}
|
20
|
+
opts[:server_side_encryption] = "AES256" if @encrypt
|
21
|
+
bucket.object(key).put(body: value, **opts)
|
22
|
+
end
|
23
|
+
|
24
|
+
def get(key)
|
25
|
+
bucket.object(key).get.body.read
|
26
|
+
end
|
27
|
+
|
28
|
+
def delete(key)
|
29
|
+
bucket.object(key).delete
|
30
|
+
end
|
31
|
+
|
32
|
+
def to_h
|
33
|
+
Hash[bucket.objects.collect {|o|
|
34
|
+
[o.key, o.get.body.read]
|
35
|
+
}]
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
def bucket
|
41
|
+
raise ArgumentError.new("The s3 secret bucket is not set") unless @bucket_name
|
42
|
+
@bucket ||= ::Aws::S3::Bucket.new(@bucket_name)
|
43
|
+
end
|
44
|
+
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,211 @@
|
|
1
|
+
require_relative '../../../atmos'
|
2
|
+
require_relative '../../../atmos/otp'
|
3
|
+
require 'aws-sdk-iam'
|
4
|
+
require 'securerandom'
|
5
|
+
|
6
|
+
module Atmos
|
7
|
+
module Providers
|
8
|
+
module Aws
|
9
|
+
|
10
|
+
class UserManager
|
11
|
+
include GemLogger::LoggerSupport
|
12
|
+
include FileUtils
|
13
|
+
|
14
|
+
def initialize(provider)
|
15
|
+
@provider = provider
|
16
|
+
end
|
17
|
+
|
18
|
+
def create_user(user_name)
|
19
|
+
result = {}
|
20
|
+
client = ::Aws::IAM::Client.new
|
21
|
+
resource = ::Aws::IAM::Resource.new
|
22
|
+
|
23
|
+
user = resource.user(user_name)
|
24
|
+
|
25
|
+
if user.exists?
|
26
|
+
logger.info "User '#{user_name}' already exists"
|
27
|
+
else
|
28
|
+
logger.info "Creating new user '#{user_name}'"
|
29
|
+
user = resource.create_user(user_name: user_name)
|
30
|
+
client.wait_until(:user_exists, user_name: user_name)
|
31
|
+
logger.debug "User created, user_name=#{user_name}"
|
32
|
+
end
|
33
|
+
|
34
|
+
result[:user_name] = user_name
|
35
|
+
|
36
|
+
return result
|
37
|
+
end
|
38
|
+
|
39
|
+
def set_groups(user_name, groups, force: false)
|
40
|
+
result = {}
|
41
|
+
resource = ::Aws::IAM::Resource.new
|
42
|
+
|
43
|
+
user = resource.user(user_name)
|
44
|
+
|
45
|
+
existing_groups = user.groups.collect(&:name)
|
46
|
+
groups_to_add = groups - existing_groups
|
47
|
+
groups_to_remove = existing_groups - groups
|
48
|
+
|
49
|
+
result[:groups] = existing_groups
|
50
|
+
|
51
|
+
groups_to_add.each do |group|
|
52
|
+
logger.debug "Adding group: #{group}"
|
53
|
+
user.add_group(group_name: group)
|
54
|
+
result[:groups] << group
|
55
|
+
end
|
56
|
+
|
57
|
+
if force
|
58
|
+
groups_to_remove.each do |group|
|
59
|
+
logger.debug "Removing group: #{group}"
|
60
|
+
user.remove_group(group_name: group)
|
61
|
+
result[:groups].delete(group)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
logger.info "User associated with groups=#{result[:groups]}"
|
66
|
+
|
67
|
+
return result
|
68
|
+
end
|
69
|
+
|
70
|
+
def enable_login(user_name, force: false)
|
71
|
+
result = {}
|
72
|
+
resource = ::Aws::IAM::Resource.new
|
73
|
+
|
74
|
+
user = resource.user(user_name)
|
75
|
+
|
76
|
+
password = ""
|
77
|
+
classes = [/[a-z]/, /[A-Z]/, /[0-9]/, /[!@#$%^&*()_+\-=\[\]{}|']/]
|
78
|
+
while ! classes.all? {|c| password =~ c }
|
79
|
+
password = SecureRandom.base64(15)
|
80
|
+
end
|
81
|
+
|
82
|
+
exists = false
|
83
|
+
begin
|
84
|
+
user.login_profile.create_date
|
85
|
+
exists = true
|
86
|
+
rescue ::Aws::IAM::Errors::NoSuchEntity
|
87
|
+
exists = false
|
88
|
+
end
|
89
|
+
|
90
|
+
if exists
|
91
|
+
logger.info "User login already exists"
|
92
|
+
if force
|
93
|
+
user.login_profile.update(password: password, password_reset_required: true)
|
94
|
+
result[:password] = password
|
95
|
+
logger.info "Updated user login with password=#{password}"
|
96
|
+
end
|
97
|
+
else
|
98
|
+
user.create_login_profile(password: password, password_reset_required: true)
|
99
|
+
result[:password] = password
|
100
|
+
logger.info "User login enabled with password=#{password}"
|
101
|
+
end
|
102
|
+
|
103
|
+
return result
|
104
|
+
end
|
105
|
+
|
106
|
+
def enable_mfa(user_name, force: false)
|
107
|
+
result = {}
|
108
|
+
client = ::Aws::IAM::Client.new
|
109
|
+
resource = ::Aws::IAM::Resource.new
|
110
|
+
|
111
|
+
user = resource.user(user_name)
|
112
|
+
|
113
|
+
if user.mfa_devices.first
|
114
|
+
logger.info "User mfa devices already exist"
|
115
|
+
if force
|
116
|
+
logger.info "Deleting old mfa devices"
|
117
|
+
user.mfa_devices.each do |dev|
|
118
|
+
dev.disassociate
|
119
|
+
client.delete_virtual_mfa_device(serial_number: dev.serial_number)
|
120
|
+
Atmos::Otp.instance.remove(user_name)
|
121
|
+
end
|
122
|
+
else
|
123
|
+
return result
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
resp = client.create_virtual_mfa_device(
|
128
|
+
virtual_mfa_device_name: user_name
|
129
|
+
)
|
130
|
+
|
131
|
+
serial = resp.virtual_mfa_device.serial_number
|
132
|
+
seed = resp.virtual_mfa_device.base_32_string_seed
|
133
|
+
|
134
|
+
Atmos::Otp.instance.add(user_name, seed)
|
135
|
+
code1 = Atmos::Otp.instance.generate(user_name)
|
136
|
+
interval = (30 - (Time.now.to_i % 30)) + 1
|
137
|
+
logger.info "Waiting for #{interval}s to generate second otp key for enablement"
|
138
|
+
sleep interval
|
139
|
+
code2 = Atmos::Otp.instance.generate(user_name)
|
140
|
+
raise "MFA codes should not be the same" if code1 == code2
|
141
|
+
|
142
|
+
resp = client.enable_mfa_device({
|
143
|
+
user_name: user_name,
|
144
|
+
serial_number: serial,
|
145
|
+
authentication_code_1: code1,
|
146
|
+
authentication_code_2: code2,
|
147
|
+
})
|
148
|
+
|
149
|
+
result[:mfa_secret] = seed
|
150
|
+
|
151
|
+
return result
|
152
|
+
end
|
153
|
+
|
154
|
+
def enable_access_keys(user_name, force: false)
|
155
|
+
result = {}
|
156
|
+
resource = ::Aws::IAM::Resource.new
|
157
|
+
|
158
|
+
user = resource.user(user_name)
|
159
|
+
|
160
|
+
if user.access_keys.first
|
161
|
+
logger.info "User access keys already exist"
|
162
|
+
if force
|
163
|
+
logger.info "Deleting old access keys"
|
164
|
+
user.access_keys.each do |key|
|
165
|
+
key.delete
|
166
|
+
end
|
167
|
+
else
|
168
|
+
return result
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
# TODO: auto add to ~/.aws/credentials and config
|
173
|
+
key_pair = user.create_access_key_pair
|
174
|
+
result[:key] = key_pair.access_key_id
|
175
|
+
result[:secret] = key_pair.secret
|
176
|
+
logger.debug "User keys generated key=#{key_pair.access_key_id}, secret=#{key_pair.secret}"
|
177
|
+
|
178
|
+
return result
|
179
|
+
end
|
180
|
+
|
181
|
+
def set_public_key(user_name, public_key, force: false)
|
182
|
+
result = {}
|
183
|
+
client = ::Aws::IAM::Client.new
|
184
|
+
resource = ::Aws::IAM::Resource.new
|
185
|
+
|
186
|
+
user = resource.user(user_name)
|
187
|
+
keys = client.list_ssh_public_keys(user_name: user_name).ssh_public_keys
|
188
|
+
if keys.size > 0
|
189
|
+
logger.info "User ssh public keys already exist"
|
190
|
+
if force
|
191
|
+
logger.info "Deleting old ssh public keys"
|
192
|
+
keys.each do |key|
|
193
|
+
client.delete_ssh_public_key(user_name: user_name,
|
194
|
+
ssh_public_key_id: key.ssh_public_key_id)
|
195
|
+
end
|
196
|
+
else
|
197
|
+
return result
|
198
|
+
end
|
199
|
+
end
|
200
|
+
|
201
|
+
client.upload_ssh_public_key(user_name: user_name, ssh_public_key_body: public_key)
|
202
|
+
logger.debug "User public key assigned: #{public_key}"
|
203
|
+
|
204
|
+
return result
|
205
|
+
end
|
206
|
+
|
207
|
+
end
|
208
|
+
|
209
|
+
end
|
210
|
+
end
|
211
|
+
end
|
@@ -0,0 +1,90 @@
|
|
1
|
+
require 'hashie'
|
2
|
+
|
3
|
+
module Atmos
|
4
|
+
|
5
|
+
class SettingsHash < Hashie::Mash
|
6
|
+
include GemLogger::LoggerSupport
|
7
|
+
include Hashie::Extensions::DeepMerge
|
8
|
+
include Hashie::Extensions::DeepFetch
|
9
|
+
disable_warnings
|
10
|
+
|
11
|
+
PATH_PATTERN = /[\.\[\]]/
|
12
|
+
|
13
|
+
def notation_get(key)
|
14
|
+
path = key.to_s.split(PATH_PATTERN).compact
|
15
|
+
path = path.collect {|p| p =~ /^\d+$/ ? p.to_i : p }
|
16
|
+
result = nil
|
17
|
+
|
18
|
+
begin
|
19
|
+
result = deep_fetch(*path)
|
20
|
+
rescue Hashie::Extensions::DeepFetch::UndefinedPathError => e
|
21
|
+
logger.debug("Settings missing value for key='#{key}'")
|
22
|
+
end
|
23
|
+
|
24
|
+
return result
|
25
|
+
end
|
26
|
+
|
27
|
+
def notation_put(key, value, additive: true)
|
28
|
+
path = key.to_s.split(PATH_PATTERN).compact
|
29
|
+
path = path.collect {|p| p =~ /^\d+$/ ? p.to_i : p }
|
30
|
+
current_level = self
|
31
|
+
path.each_with_index do |p, i|
|
32
|
+
|
33
|
+
if i == path.size - 1
|
34
|
+
if additive && current_level[p].is_a?(Array)
|
35
|
+
current_level[p] = current_level[p] | Array(value)
|
36
|
+
else
|
37
|
+
current_level[p] = value
|
38
|
+
end
|
39
|
+
else
|
40
|
+
if current_level[p].nil?
|
41
|
+
if path[i+1].is_a?(Integer)
|
42
|
+
current_level[p] = []
|
43
|
+
else
|
44
|
+
current_level[p] = {}
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
current_level = current_level[p]
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def self.add_config(yml_file, key, value, additive: true)
|
54
|
+
orig_config_with_comments = File.read(yml_file)
|
55
|
+
|
56
|
+
comment_places = {}
|
57
|
+
comment_lines = []
|
58
|
+
orig_config_with_comments.each_line do |line|
|
59
|
+
line.gsub!(/\s+$/, "\n")
|
60
|
+
if line =~ /^\s*(#.*)?$/
|
61
|
+
comment_lines << line
|
62
|
+
else
|
63
|
+
if comment_lines.present?
|
64
|
+
comment_places[line.chomp] = comment_lines
|
65
|
+
comment_lines = []
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
comment_places["<EOF>"] = comment_lines
|
70
|
+
|
71
|
+
orig_config = SettingsHash.new((YAML.load_file(yml_file) rescue {}))
|
72
|
+
orig_config.notation_put(key, value, additive: additive)
|
73
|
+
new_config_no_comments = YAML.dump(orig_config.to_hash)
|
74
|
+
new_config_no_comments.sub!(/\A---\n/, "")
|
75
|
+
|
76
|
+
new_yml = ""
|
77
|
+
new_config_no_comments.each_line do |line|
|
78
|
+
line.gsub!(/\s+$/, "\n")
|
79
|
+
cline = comment_places.keys.find {|k| line =~ /^#{k}/ }
|
80
|
+
comments = comment_places[cline]
|
81
|
+
comments.each {|comment| new_yml << comment } if comments
|
82
|
+
new_yml << line
|
83
|
+
end
|
84
|
+
comment_places["<EOF>"].each {|comment| new_yml << comment }
|
85
|
+
|
86
|
+
return new_yml
|
87
|
+
end
|
88
|
+
|
89
|
+
end
|
90
|
+
end
|
@@ -0,0 +1,267 @@
|
|
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 Atmos
|
10
|
+
|
11
|
+
class TerraformExecutor
|
12
|
+
include GemLogger::LoggerSupport
|
13
|
+
include FileUtils
|
14
|
+
include Atmos::UI
|
15
|
+
|
16
|
+
class ProcessFailed < RuntimeError; end
|
17
|
+
|
18
|
+
def initialize(process_env: ENV, working_group: 'default')
|
19
|
+
@process_env = process_env
|
20
|
+
@working_group = working_group
|
21
|
+
@working_dir = Atmos.config.tf_working_dir(@working_group)
|
22
|
+
@recipes = Atmos.config["recipes.#{@working_group}"]
|
23
|
+
end
|
24
|
+
|
25
|
+
def run(*terraform_args, skip_backend: false, skip_secrets: false, get_modules: false, output_io: nil)
|
26
|
+
setup_working_dir(skip_backend: skip_backend)
|
27
|
+
|
28
|
+
if get_modules
|
29
|
+
logger.debug("Getting modules")
|
30
|
+
get_modules_io = StringIO.new
|
31
|
+
begin
|
32
|
+
execute("get", output_io: get_modules_io)
|
33
|
+
rescue Atmos::TerraformExecutor::ProcessFailed => e
|
34
|
+
logger.info(get_modules_io.string)
|
35
|
+
raise
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
return execute(*terraform_args, skip_secrets: skip_secrets, output_io: output_io)
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
def tf_cmd(*args)
|
45
|
+
['terraform'] + args
|
46
|
+
end
|
47
|
+
|
48
|
+
def execute(*terraform_args, skip_secrets: false, output_io: nil)
|
49
|
+
cmd = tf_cmd(*terraform_args)
|
50
|
+
logger.debug("Running terraform: #{cmd.join(' ')}")
|
51
|
+
|
52
|
+
env = Hash[@process_env]
|
53
|
+
if ! skip_secrets
|
54
|
+
begin
|
55
|
+
env = env.merge(secrets_env)
|
56
|
+
rescue => e
|
57
|
+
logger.debug("Secrets not available: #{e}")
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
# lets tempfiles create by subprocesses be easily found by users
|
62
|
+
env['TMPDIR'] = Atmos.config.tmp_dir
|
63
|
+
|
64
|
+
# Lets terraform communicate back to atmos, e.g. for UI notifications
|
65
|
+
ipc = Atmos::Ipc.new(Atmos.config.tmp_dir)
|
66
|
+
|
67
|
+
IO.pipe do |stdout, stdout_writer|
|
68
|
+
IO.pipe do |stderr, stderr_writer|
|
69
|
+
|
70
|
+
stdout_writer.sync = stderr_writer.sync = true
|
71
|
+
# TODO: more filtering on terraform output?
|
72
|
+
stdout_thr = pipe_stream(stdout, output_io.nil? ? $stdout : output_io) do |data|
|
73
|
+
if data =~ /^[\e\[\dm\s]*Enter a value:[\e\[\dm\s]*$/
|
74
|
+
notify(message: "Terraform is waiting for user input")
|
75
|
+
end
|
76
|
+
data
|
77
|
+
end
|
78
|
+
stderr_thr = pipe_stream(stderr, output_io.nil? ? $stderr : output_io)
|
79
|
+
|
80
|
+
ipc.listen do |sock_path|
|
81
|
+
|
82
|
+
if Atmos.config['ipc.disable']
|
83
|
+
# Using : as the command makes execution of ipc from the
|
84
|
+
# terraform side a no-op in both cases of how we call it. This
|
85
|
+
# way, terraform execution continues to work when IPC is disabled
|
86
|
+
# command = "$ATMOS_IPC_CLIENT <json_string>"
|
87
|
+
# program = ["sh", "-c", "$ATMOS_IPC_CLIENT"]
|
88
|
+
env['ATMOS_IPC_CLIENT'] = ":"
|
89
|
+
else
|
90
|
+
env['ATMOS_IPC_SOCK'] = sock_path
|
91
|
+
env['ATMOS_IPC_CLIENT'] = ipc.generate_client_script
|
92
|
+
end
|
93
|
+
|
94
|
+
# Was unable to get piping to work with stdin for some reason. It
|
95
|
+
# worked in simple case, but started to fail when terraform config
|
96
|
+
# got more extensive. Thus, using spawn to redirect stdin from the
|
97
|
+
# terminal direct to terraform, with IO.pipe to copy the outher
|
98
|
+
# streams. Maybe in the future we can completely disconnect stdin
|
99
|
+
# and have atmos do the output parsing and stdin prompting
|
100
|
+
pid = spawn(env, *cmd,
|
101
|
+
chdir: tf_recipes_dir,
|
102
|
+
:out=>stdout_writer, :err=> stderr_writer, :in => :in)
|
103
|
+
|
104
|
+
logger.debug("Terraform started with pid #{pid}")
|
105
|
+
Process.wait(pid)
|
106
|
+
end
|
107
|
+
|
108
|
+
stdout_writer.close
|
109
|
+
stderr_writer.close
|
110
|
+
stdout_thr.join
|
111
|
+
stderr_thr.join
|
112
|
+
|
113
|
+
status = $?.exitstatus
|
114
|
+
logger.debug("Terraform exited: #{status}")
|
115
|
+
if status != 0
|
116
|
+
raise ProcessFailed.new "Terraform exited with non-zero exit code: #{status}"
|
117
|
+
end
|
118
|
+
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
end
|
123
|
+
|
124
|
+
def setup_working_dir(skip_backend: false)
|
125
|
+
clean_links
|
126
|
+
link_shared_plugin_dir
|
127
|
+
link_support_dirs
|
128
|
+
link_recipes
|
129
|
+
write_atmos_vars
|
130
|
+
setup_backend(skip_backend)
|
131
|
+
end
|
132
|
+
|
133
|
+
def setup_backend(skip_backend=false)
|
134
|
+
backend_file = File.join(tf_recipes_dir, 'atmos-backend.tf.json')
|
135
|
+
backend_config = (Atmos.config["backend"] || {}).clone
|
136
|
+
|
137
|
+
if backend_config.present? && ! skip_backend
|
138
|
+
logger.debug("Writing out terraform state backend config")
|
139
|
+
|
140
|
+
# Use a different state file per group
|
141
|
+
if @working_group
|
142
|
+
backend_config['key'] = "#{@working_group}-#{backend_config['key']}"
|
143
|
+
end
|
144
|
+
|
145
|
+
backend_type = backend_config.delete("type")
|
146
|
+
|
147
|
+
backend = {
|
148
|
+
"terraform" => {
|
149
|
+
"backend" => {
|
150
|
+
backend_type => backend_config
|
151
|
+
}
|
152
|
+
}
|
153
|
+
}
|
154
|
+
|
155
|
+
File.write(backend_file, JSON.pretty_generate(backend))
|
156
|
+
else
|
157
|
+
logger.debug("Clearing terraform state backend config")
|
158
|
+
File.delete(backend_file) if File.exist?(backend_file)
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
# terraform currently (v0.11.3) doesn't handle maps with nested maps or
|
163
|
+
# lists well, so flatten them - nested maps get expanded into the top level
|
164
|
+
# one, with their keys being appended with underscores, and lists get
|
165
|
+
# joined with "," so we end up with a single hash with homogenous types
|
166
|
+
def homogenize_for_terraform(h, root={}, prefix="")
|
167
|
+
h.each do |k, v|
|
168
|
+
if v.is_a? Hash
|
169
|
+
homogenize_for_terraform(v, root, "#{k}_")
|
170
|
+
else
|
171
|
+
v = v.join(",") if v.is_a? Array
|
172
|
+
root["#{prefix}#{k}"] = v
|
173
|
+
end
|
174
|
+
end
|
175
|
+
return root
|
176
|
+
end
|
177
|
+
|
178
|
+
def tf_recipes_dir
|
179
|
+
@tf_recipes_dir ||= begin
|
180
|
+
dir = File.join(@working_dir, 'recipes')
|
181
|
+
logger.debug("Tf recipes dir: #{dir}")
|
182
|
+
mkdir_p(dir)
|
183
|
+
dir
|
184
|
+
end
|
185
|
+
end
|
186
|
+
|
187
|
+
def write_atmos_vars
|
188
|
+
File.open(File.join(tf_recipes_dir, 'atmos.auto.tfvars.json'), 'w') do |f|
|
189
|
+
atmos_var_config = atmos_config = homogenize_for_terraform(Atmos.config.to_h)
|
190
|
+
|
191
|
+
var_prefix = Atmos.config['var_prefix']
|
192
|
+
if var_prefix
|
193
|
+
atmos_var_config = Hash[atmos_var_config.collect {|k, v| ["#{var_prefix}#{k}", v]}]
|
194
|
+
end
|
195
|
+
|
196
|
+
var_hash = {
|
197
|
+
atmos_env: Atmos.config.atmos_env,
|
198
|
+
all_env_names: Atmos.config.all_env_names,
|
199
|
+
account_ids: Atmos.config.account_hash,
|
200
|
+
atmos_config: atmos_config
|
201
|
+
}
|
202
|
+
var_hash = var_hash.merge(atmos_var_config)
|
203
|
+
f.puts(JSON.pretty_generate(var_hash))
|
204
|
+
end
|
205
|
+
end
|
206
|
+
|
207
|
+
def secrets_env
|
208
|
+
# NOTE use an auto-deleting temp file if passing secrets through env ends
|
209
|
+
# up being problematic
|
210
|
+
# TODO fix the need for CC - TE calls for secrets which needs auth in
|
211
|
+
# ENV, so kinda clunk to have to do both CC and pass the env in
|
212
|
+
ClimateControl.modify(@process_env) do
|
213
|
+
secrets = Atmos.config.provider.secret_manager.to_h
|
214
|
+
env_secrets = Hash[secrets.collect { |k, v| ["TF_VAR_#{k}", v] }]
|
215
|
+
return env_secrets
|
216
|
+
end
|
217
|
+
end
|
218
|
+
|
219
|
+
def clean_links
|
220
|
+
Find.find(@working_dir) do |f|
|
221
|
+
Find.prune if f =~ /\/.terraform\/modules\//
|
222
|
+
File.delete(f) if File.symlink?(f)
|
223
|
+
end
|
224
|
+
end
|
225
|
+
|
226
|
+
def link_support_dirs
|
227
|
+
['modules', 'templates'].each do |subdir|
|
228
|
+
ln_sf(File.join(Atmos.config.root_dir, subdir), @working_dir)
|
229
|
+
end
|
230
|
+
end
|
231
|
+
|
232
|
+
def link_shared_plugin_dir
|
233
|
+
if ! Atmos.config["terraform.disable_shared_plugins"]
|
234
|
+
shared_plugins_dir = File.join(Atmos.config.tmp_root, "terraform_plugins")
|
235
|
+
mkdir_p(shared_plugins_dir)
|
236
|
+
terraform_state_dir = File.join(tf_recipes_dir, '.terraform')
|
237
|
+
mkdir_p(terraform_state_dir)
|
238
|
+
terraform_plugins_dir = File.join(terraform_state_dir, 'plugins')
|
239
|
+
ln_sf(shared_plugins_dir, terraform_plugins_dir)
|
240
|
+
end
|
241
|
+
end
|
242
|
+
|
243
|
+
def link_recipes
|
244
|
+
@recipes.each do |recipe|
|
245
|
+
ln_sf(File.join(Atmos.config.root_dir, 'recipes', "#{recipe}.tf"), tf_recipes_dir)
|
246
|
+
end
|
247
|
+
end
|
248
|
+
|
249
|
+
def pipe_stream(src, dest)
|
250
|
+
Thread.new do
|
251
|
+
block_size = 1024
|
252
|
+
begin
|
253
|
+
while data = src.readpartial(block_size)
|
254
|
+
data = yield data if block_given?
|
255
|
+
dest.write(data)
|
256
|
+
end
|
257
|
+
rescue EOFError
|
258
|
+
nil
|
259
|
+
rescue Exception => e
|
260
|
+
logger.log_exception(e, "Stream failure")
|
261
|
+
end
|
262
|
+
end
|
263
|
+
end
|
264
|
+
|
265
|
+
end
|
266
|
+
|
267
|
+
end
|