tfctl 0.0.2 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rubocop.yml +79 -0
- data/CHANGELOG.adoc +4 -0
- data/Gemfile +2 -0
- data/Makefile +8 -2
- data/README.adoc +17 -7
- data/bin/tfctl +76 -56
- data/examples/control_tower/conf/example.yaml +1 -1
- data/lib/hash.rb +17 -11
- data/lib/tfctl/aws_org.rb +42 -51
- data/lib/tfctl/config.rb +23 -22
- data/lib/tfctl/executor.rb +15 -23
- data/lib/tfctl/generator.rb +20 -21
- data/lib/tfctl/logger.rb +20 -6
- data/lib/tfctl/version.rb +1 -1
- data/tfctl.gemspec +13 -8
- metadata +33 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: cac739f38f0484a3f0a8373224183f0705c79372962c58711f82719978d86fbe
|
4
|
+
data.tar.gz: 8ed33267e070125bb03e1591075642163c367f8d5ab1d4ee4040c2afc97a11d8
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 675c2b7e868b2850749a7ca53b34ee594f84a8b30a62f3098ede1221394879d7595ecede9b76eb3da960728600092f1385ac35d122e5d2aaf17e699f903d019a
|
7
|
+
data.tar.gz: caf57a862a8e5ecc2e3ff64102389f1822527f4975a708407b75a23a2378d8a087fe1a6ead434f4835fc3e5681c35c145d5d5b6dfb446409ffc909940a34c2e7
|
data/.rubocop.yml
ADDED
@@ -0,0 +1,79 @@
|
|
1
|
+
---
|
2
|
+
AllCops:
|
3
|
+
TargetRubyVersion: 2.3
|
4
|
+
DisplayCopNames: true
|
5
|
+
|
6
|
+
Layout/IndentationWidth:
|
7
|
+
Width: 4
|
8
|
+
|
9
|
+
Layout/IndentHeredoc:
|
10
|
+
Enabled: false
|
11
|
+
|
12
|
+
Layout/EmptyLines:
|
13
|
+
Enabled: false
|
14
|
+
|
15
|
+
Layout/EmptyLinesAroundMethodBody:
|
16
|
+
Enabled: false
|
17
|
+
|
18
|
+
Layout/AlignHash:
|
19
|
+
EnforcedHashRocketStyle:
|
20
|
+
- table
|
21
|
+
EnforcedColonStyle:
|
22
|
+
- table
|
23
|
+
|
24
|
+
Layout/SpaceAroundOperators:
|
25
|
+
Enabled: false
|
26
|
+
|
27
|
+
Layout/ExtraSpacing:
|
28
|
+
Enabled: false
|
29
|
+
|
30
|
+
Layout/EmptyLinesAroundBlockBody:
|
31
|
+
Enabled: false
|
32
|
+
|
33
|
+
Layout/EmptyLinesAroundClassBody:
|
34
|
+
Enabled: false
|
35
|
+
|
36
|
+
Metrics/CyclomaticComplexity:
|
37
|
+
Enabled: false
|
38
|
+
|
39
|
+
Metrics/PerceivedComplexity:
|
40
|
+
Enabled: false
|
41
|
+
|
42
|
+
Metrics/BlockLength:
|
43
|
+
Enabled: false
|
44
|
+
|
45
|
+
Metrics/MethodLength:
|
46
|
+
Enabled: false
|
47
|
+
|
48
|
+
Metrics/LineLength:
|
49
|
+
Max: 140
|
50
|
+
|
51
|
+
Metrics/AbcSize:
|
52
|
+
Enabled: false
|
53
|
+
|
54
|
+
Metrics/ParameterLists:
|
55
|
+
Enabled: false
|
56
|
+
|
57
|
+
Metrics/ClassLength:
|
58
|
+
Enabled: false
|
59
|
+
|
60
|
+
Style/IfUnlessModifier:
|
61
|
+
Enabled: false
|
62
|
+
|
63
|
+
Style/AndOr:
|
64
|
+
Enabled: false
|
65
|
+
|
66
|
+
Style/Documentation:
|
67
|
+
Enabled: false
|
68
|
+
|
69
|
+
Style/TrailingCommaInArguments:
|
70
|
+
EnforcedStyleForMultiline: comma
|
71
|
+
|
72
|
+
Style/TrailingCommaInArrayLiteral:
|
73
|
+
EnforcedStyleForMultiline: comma
|
74
|
+
|
75
|
+
Style/TrailingCommaInHashLiteral:
|
76
|
+
EnforcedStyleForMultiline: comma
|
77
|
+
|
78
|
+
Style/RedundantReturn:
|
79
|
+
Enabled: false
|
data/CHANGELOG.adoc
CHANGED
data/Gemfile
CHANGED
data/Makefile
CHANGED
@@ -1,10 +1,16 @@
|
|
1
|
-
.PHONY: clean install test
|
1
|
+
.PHONY: clean install test rubocop spec
|
2
2
|
|
3
3
|
vendor:
|
4
4
|
$(info => Installing Ruby dependencies)
|
5
5
|
@bundle install --path vendor --with developement --binstubs=vendor/bin
|
6
6
|
|
7
|
-
test: vendor
|
7
|
+
test: vendor rubocop spec
|
8
|
+
|
9
|
+
rubocop:
|
10
|
+
$(info => Running rubocop)
|
11
|
+
@vendor/bin/rubocop
|
12
|
+
|
13
|
+
spec:
|
8
14
|
$(info => Running spec tests)
|
9
15
|
@vendor/bin/rspec
|
10
16
|
|
data/README.adoc
CHANGED
@@ -2,6 +2,9 @@
|
|
2
2
|
|
3
3
|
= tfctl
|
4
4
|
|
5
|
+
image:https://travis-ci.org/scalefactory/tfctl.svg?branch=master["Build Status", link="https://travis-ci.org/scalefactory/tfctl"]
|
6
|
+
image:https://badge.fury.io/rb/tfctl.svg["Gem Version", link="https://badge.fury.io/rb/tfctl"]
|
7
|
+
|
5
8
|
== Overview
|
6
9
|
|
7
10
|
Tfctl is a small Terraform wrapper for working with multi-account AWS
|
@@ -39,18 +42,16 @@ other ways of managing accounts in AWS Organizations.
|
|
39
42
|
|
40
43
|
== Installation
|
41
44
|
|
42
|
-
|
45
|
+
To install the latest release from RubyGems run:
|
43
46
|
|
44
47
|
----
|
45
|
-
|
48
|
+
gem install tfctl
|
46
49
|
----
|
47
50
|
|
48
|
-
|
49
|
-
|
50
|
-
When using bundler, add this to your `Gemfile`:
|
51
|
+
Alternatively you can build and install from this repo with:
|
51
52
|
|
52
53
|
----
|
53
|
-
|
54
|
+
make install
|
54
55
|
----
|
55
56
|
|
56
57
|
== Docs
|
@@ -97,7 +98,16 @@ Show merged configuration:
|
|
97
98
|
tfctl -c conf/example.yaml -s
|
98
99
|
----
|
99
100
|
|
100
|
-
|
101
|
+
List all discovered accounts:
|
102
|
+
|
103
|
+
----
|
104
|
+
tfctl -c conf/example.yaml --all -l
|
105
|
+
----
|
106
|
+
|
107
|
+
TIP: This can be narrowed down using targeting options and is a good way to
|
108
|
+
test what accounts match.
|
109
|
+
|
110
|
+
Run Terraform init across all accounts:
|
101
111
|
|
102
112
|
----
|
103
113
|
tfctl -c conf/example.yaml --all -- init
|
data/bin/tfctl
CHANGED
@@ -7,6 +7,8 @@ end
|
|
7
7
|
require 'optparse'
|
8
8
|
require 'fileutils'
|
9
9
|
require 'parallel'
|
10
|
+
require 'English'
|
11
|
+
require 'terminal-table'
|
10
12
|
require_relative '../lib/tfctl'
|
11
13
|
|
12
14
|
PROJECT_ROOT = Dir.pwd
|
@@ -15,43 +17,46 @@ PROJECT_ROOT = Dir.pwd
|
|
15
17
|
# Process CLI arguments
|
16
18
|
#
|
17
19
|
|
18
|
-
options={
|
19
|
-
:
|
20
|
-
:
|
21
|
-
:
|
22
|
-
:
|
23
|
-
:
|
24
|
-
:
|
25
|
-
:
|
26
|
-
:
|
20
|
+
options = {
|
21
|
+
account: nil,
|
22
|
+
ou: nil,
|
23
|
+
all: nil,
|
24
|
+
show_config: false,
|
25
|
+
config_file: nil,
|
26
|
+
unbuffered: false,
|
27
|
+
debug: false,
|
28
|
+
use_cache: false,
|
27
29
|
}
|
28
30
|
|
29
31
|
optparse = OptionParser.new do |opts|
|
30
|
-
opts.on('-a', '--account=name',
|
32
|
+
opts.on('-a', '--account=name', 'Target a specific AWS account') do |o|
|
31
33
|
options[:account] = o
|
32
34
|
end
|
33
|
-
opts.on('-o', '--ou=organization_unit',
|
35
|
+
opts.on('-o', '--ou=organization_unit', 'Target accounts in an Organization Unit (uses regex matching)') do |o|
|
34
36
|
options[:ou] = o
|
35
37
|
end
|
36
|
-
opts.on('--all',
|
38
|
+
opts.on('--all', 'Target all accounts') do
|
37
39
|
options[:all] = true
|
38
40
|
end
|
39
|
-
opts.on('-c', '--config-file=config',
|
41
|
+
opts.on('-c', '--config-file=config', 'Path to config file') do |o|
|
40
42
|
options[:config_file] = o
|
41
43
|
end
|
42
|
-
opts.on('-s', '--show-config',
|
44
|
+
opts.on('-s', '--show-config', 'Display configuration') do
|
43
45
|
options[:show_config] = true
|
44
46
|
end
|
45
|
-
opts.on('-
|
47
|
+
opts.on('-l', '--list-accounts', 'List discovered accounts') do
|
48
|
+
options[:list_accounts] = true
|
49
|
+
end
|
50
|
+
opts.on('-x', '--use-cache', 'Use cached AWS organization data') do
|
46
51
|
options[:use_cache] = true
|
47
52
|
end
|
48
|
-
opts.on('-u', '--unbuffered',
|
53
|
+
opts.on('-u', '--unbuffered', 'Disable buffering of Terraform output') do
|
49
54
|
options[:unbuffered] = true
|
50
55
|
end
|
51
|
-
opts.on('-d', '--debug',
|
56
|
+
opts.on('-d', '--debug', 'Turn on debug messages') do
|
52
57
|
options[:debug] = true
|
53
58
|
end
|
54
|
-
opts.on('-v', '--version',
|
59
|
+
opts.on('-v', '--version', 'Show version') do
|
55
60
|
puts Tfctl::VERSION
|
56
61
|
exit
|
57
62
|
end
|
@@ -67,34 +72,33 @@ begin
|
|
67
72
|
raise OptionParser::MissingArgument, '--config-file'
|
68
73
|
end
|
69
74
|
|
70
|
-
unless File.
|
75
|
+
unless File.exist? options[:config_file]
|
71
76
|
raise OptionParser::InvalidOption,
|
72
|
-
|
77
|
+
"Config file not found in: #{options[:config_file]}"
|
73
78
|
end
|
74
79
|
|
75
|
-
unless File.
|
76
|
-
|
80
|
+
unless File.exist? options[:config_file]
|
81
|
+
raise OptionParser::InvalidOption, "Config file #{options[:config_file]} not found."
|
77
82
|
end
|
78
83
|
|
79
84
|
# Validate targets
|
80
|
-
targetting_opts = [
|
85
|
+
targetting_opts = %i[account ou all]
|
81
86
|
targets_set = []
|
82
|
-
options.each do |k,v|
|
87
|
+
options.each do |k, v|
|
83
88
|
if targetting_opts.include?(k)
|
84
|
-
targets_set << k.to_s unless v
|
89
|
+
targets_set << k.to_s unless v.nil?
|
85
90
|
end
|
86
91
|
end
|
87
92
|
if targets_set.length > 1
|
88
93
|
raise OptionParser::InvalidOption,
|
89
|
-
|
94
|
+
"Too many target options set: #{targets_set.join(', ')}. Only one can be specified."
|
90
95
|
end
|
91
96
|
if targets_set.empty? and options[:show_config] == false
|
92
|
-
raise OptionParser::InvalidOption,
|
97
|
+
raise OptionParser::InvalidOption, 'Please specify target'
|
93
98
|
end
|
94
|
-
|
95
99
|
rescue OptionParser::InvalidOption, OptionParser::MissingArgument
|
96
|
-
$
|
97
|
-
|
100
|
+
warn $ERROR_INFO.to_s
|
101
|
+
warn optparse
|
98
102
|
exit 2
|
99
103
|
end
|
100
104
|
|
@@ -114,12 +118,11 @@ def run_account(config, account, options, tf_argv, log)
|
|
114
118
|
# executed from.
|
115
119
|
log.info "#{account[:name]}: Generating Terraform run directory"
|
116
120
|
Tfctl::Generator.make(
|
117
|
-
config:
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
execution_role: account[:tf_execution_role]
|
121
|
+
config: config,
|
122
|
+
account_id: account[:id],
|
123
|
+
account_name: account[:name],
|
124
|
+
profiles: account[:profiles],
|
125
|
+
execution_role: account[:tf_execution_role],
|
123
126
|
)
|
124
127
|
|
125
128
|
log.info "#{account[:name]}: Executing Terraform #{tf_argv[0]}"
|
@@ -128,7 +131,7 @@ def run_account(config, account, options, tf_argv, log)
|
|
128
131
|
config_name: config[:config_name],
|
129
132
|
unbuffered: options[:unbuffered],
|
130
133
|
log: log,
|
131
|
-
argv: tf_argv
|
134
|
+
argv: tf_argv,
|
132
135
|
)
|
133
136
|
end
|
134
137
|
|
@@ -138,9 +141,8 @@ end
|
|
138
141
|
#
|
139
142
|
|
140
143
|
begin
|
141
|
-
|
142
144
|
# Set up logging
|
143
|
-
options[:debug] ?
|
145
|
+
log_level = options[:debug] ? Logger::DEBUG : Logger::INFO
|
144
146
|
log = Tfctl::Logger.new(log_level)
|
145
147
|
|
146
148
|
log.info 'tfctl running'
|
@@ -150,7 +152,7 @@ begin
|
|
150
152
|
|
151
153
|
log.info 'Working out AWS account topology'
|
152
154
|
|
153
|
-
yaml_config = YAML.
|
155
|
+
yaml_config = YAML.safe_load(File.read(options[:config_file]))
|
154
156
|
yaml_config.symbolize_names!
|
155
157
|
|
156
158
|
org_units = yaml_config[:organization_units].keys
|
@@ -159,39 +161,57 @@ begin
|
|
159
161
|
log.info 'Merging configuration'
|
160
162
|
|
161
163
|
config = Tfctl::Config.new(
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
164
|
+
config_name: config_name,
|
165
|
+
yaml_config: yaml_config,
|
166
|
+
aws_org_config: aws_org_accounts,
|
167
|
+
use_cache: options[:use_cache],
|
168
|
+
)
|
167
169
|
|
168
170
|
if options[:show_config]
|
169
171
|
puts config.to_yaml
|
170
172
|
exit 0
|
171
173
|
end
|
172
174
|
|
173
|
-
#
|
175
|
+
# Find target accounts
|
174
176
|
|
175
177
|
if options[:account]
|
176
|
-
|
177
|
-
run_account(config, account, options, ARGV, log)
|
178
|
-
|
178
|
+
accounts = config.find_accounts(:name, options[:account])
|
179
179
|
elsif options[:ou]
|
180
180
|
accounts = config.find_accounts_regex(:ou_path, options[:ou])
|
181
|
-
Parallel.each(accounts, in_processes: 8) do |ac|
|
182
|
-
run_account(config, ac, options, ARGV, log)
|
183
|
-
end
|
184
|
-
|
185
181
|
elsif options[:all]
|
186
182
|
accounts = config[:accounts]
|
187
|
-
|
188
|
-
|
183
|
+
else
|
184
|
+
raise Tfctl::Error, 'Missing target'
|
185
|
+
end
|
186
|
+
|
187
|
+
# List target accounts
|
188
|
+
|
189
|
+
if options[:list_accounts]
|
190
|
+
log.info "Listing accounts\n"
|
191
|
+
table = Terminal::Table.new do |t|
|
192
|
+
t.style = {
|
193
|
+
border_x: '',
|
194
|
+
border_y: '',
|
195
|
+
border_i: '',
|
196
|
+
padding_left: 0,
|
197
|
+
}
|
198
|
+
t << %w[ACCOUNT_ID OU NAME]
|
199
|
+
accounts.each do |account|
|
200
|
+
t << [account[:id], account[:ou_path], account[:name]]
|
201
|
+
end
|
189
202
|
end
|
203
|
+
|
204
|
+
puts table
|
205
|
+
exit 0
|
190
206
|
end
|
191
207
|
|
208
|
+
# Execute Terraform in target accounts
|
192
209
|
|
193
|
-
|
210
|
+
Parallel.each(accounts, in_processes: 8) do |ac|
|
211
|
+
run_account(config, ac, options, ARGV, log)
|
212
|
+
end
|
194
213
|
|
214
|
+
log.info 'Done'
|
195
215
|
rescue Tfctl::Error => e
|
196
216
|
log.error(e)
|
197
217
|
exit 1
|
@@ -50,7 +50,7 @@ organization_root:
|
|
50
50
|
# Configuration to apply to accounts in Organization Units
|
51
51
|
# Units not listed here will be ignored
|
52
52
|
organization_units:
|
53
|
-
# Uncomment if you want to include
|
53
|
+
# Uncomment if you want to include Core OU accounts
|
54
54
|
# Core: {}
|
55
55
|
live: {}
|
56
56
|
test: {}
|
data/lib/hash.rb
CHANGED
@@ -2,25 +2,31 @@
|
|
2
2
|
|
3
3
|
# Add a deep_merge method to a Hash.
|
4
4
|
# It unions arrays (for terraform profiles behaviour)
|
5
|
-
class
|
5
|
+
class Hash
|
6
6
|
def deep_merge(second)
|
7
|
-
merger = proc { |
|
8
|
-
|
9
|
-
|
10
|
-
|
7
|
+
merger = proc { |_key, v1, v2|
|
8
|
+
if v1.is_a?(Hash) && v2.is_a?(Hash)
|
9
|
+
v1.merge(v2, &merger)
|
10
|
+
elsif v1.is_a?(Array) && v2.is_a?(Array)
|
11
|
+
v1 | v2
|
12
|
+
elsif [:undefined, nil, :nil].include?(v2)
|
13
|
+
v1
|
14
|
+
else
|
15
|
+
v2
|
16
|
+
end
|
11
17
|
}
|
12
|
-
|
18
|
+
merge(second.to_h, &merger)
|
13
19
|
end
|
14
20
|
|
15
21
|
# Copied from ruby 2.6 Psych for 2.3 compatibility.
|
16
|
-
def symbolize_names!(result=self)
|
22
|
+
def symbolize_names!(result = self)
|
17
23
|
case result
|
18
24
|
when Hash
|
19
|
-
|
20
|
-
|
21
|
-
|
25
|
+
result.keys.each do |key|
|
26
|
+
result[key.to_sym] = symbolize_names!(result.delete(key))
|
27
|
+
end
|
22
28
|
when Array
|
23
|
-
|
29
|
+
result.map! { |r| symbolize_names!(r) }
|
24
30
|
end
|
25
31
|
result
|
26
32
|
end
|
data/lib/tfctl/aws_org.rb
CHANGED
@@ -8,39 +8,35 @@ module Tfctl
|
|
8
8
|
|
9
9
|
def initialize(role_arn)
|
10
10
|
@aws_org_client = Aws::Organizations::Client.new(
|
11
|
-
|
12
|
-
|
13
|
-
|
11
|
+
region: 'us-east-1',
|
12
|
+
# Assume role in primary account to read AWS organization API
|
13
|
+
credentials: aws_assume_role(role_arn),
|
14
14
|
)
|
15
15
|
end
|
16
16
|
|
17
17
|
# Gets account data for specified OUs from AWS Organizations API
|
18
18
|
def accounts(org_units)
|
19
|
-
output = { :
|
19
|
+
output = { accounts: [] }
|
20
20
|
|
21
21
|
aws_ou_ids = aws_ou_list
|
22
22
|
|
23
23
|
org_units.each do |ou_path|
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
:ou_parents => ou_path.to_s.split('/'),
|
41
|
-
:profiles => [],
|
42
|
-
}
|
43
|
-
end
|
24
|
+
raise Tfctl::Error, "Error: OU: #{ou_path}, does not exists in AWS organization" unless aws_ou_ids.key?(ou_path)
|
25
|
+
|
26
|
+
parent_id = aws_ou_ids[ou_path]
|
27
|
+
|
28
|
+
@aws_org_client.list_accounts_for_parent(parent_id: parent_id).accounts.each do |account|
|
29
|
+
next unless account.status == 'ACTIVE'
|
30
|
+
|
31
|
+
output[:accounts] << {
|
32
|
+
name: account.name,
|
33
|
+
id: account.id,
|
34
|
+
arn: account.arn,
|
35
|
+
email: account.email,
|
36
|
+
ou_path: ou_path.to_s,
|
37
|
+
ou_parents: ou_path.to_s.split('/'),
|
38
|
+
profiles: [],
|
39
|
+
}
|
44
40
|
end
|
45
41
|
end
|
46
42
|
output
|
@@ -49,7 +45,7 @@ module Tfctl
|
|
49
45
|
private
|
50
46
|
|
51
47
|
# Get a mapping of ou_name => ou_id from AWS organizations
|
52
|
-
def aws_ou_list
|
48
|
+
def aws_ou_list
|
53
49
|
output = {}
|
54
50
|
root_ou_id = @aws_org_client.list_roots.roots[0].id
|
55
51
|
|
@@ -62,7 +58,7 @@ module Tfctl
|
|
62
58
|
end
|
63
59
|
end
|
64
60
|
end
|
65
|
-
ou_recurse.call(
|
61
|
+
ou_recurse.call(root: root_ou_id)
|
66
62
|
|
67
63
|
output
|
68
64
|
end
|
@@ -72,28 +68,24 @@ module Tfctl
|
|
72
68
|
output = {}
|
73
69
|
retries = 0
|
74
70
|
|
75
|
-
@aws_org_client.list_children(
|
71
|
+
@aws_org_client.list_children(
|
76
72
|
child_type: 'ORGANIZATIONAL_UNIT',
|
77
|
-
parent_id:
|
78
|
-
|
73
|
+
parent_id: parent_id,
|
74
|
+
).children.each do |child|
|
79
75
|
|
80
76
|
begin
|
81
|
-
ou = @aws_org_client.describe_organizational_unit(
|
82
|
-
organizational_unit_id: child.id
|
83
|
-
|
77
|
+
ou = @aws_org_client.describe_organizational_unit(
|
78
|
+
organizational_unit_id: child.id,
|
79
|
+
).organizational_unit
|
84
80
|
rescue Aws::Organizations::Errors::TooManyRequestsException
|
85
|
-
# FIXME - use logger
|
81
|
+
# FIXME: - use logger
|
86
82
|
puts 'AWS Organizations: too many requests. Retrying in 5 secs.'
|
87
83
|
sleep 5
|
88
84
|
retries += 1
|
89
85
|
retry if retries < 10
|
90
86
|
end
|
91
87
|
|
92
|
-
|
93
|
-
ou_name = ou.name.to_sym
|
94
|
-
else
|
95
|
-
ou_name = "#{parent_name}/#{ou.name}".to_sym
|
96
|
-
end
|
88
|
+
ou_name = parent_name == :root ? ou.name.to_sym : "#{parent_name}/#{ou.name}".to_sym
|
97
89
|
|
98
90
|
output[ou_name] = ou.id
|
99
91
|
end
|
@@ -101,20 +93,19 @@ module Tfctl
|
|
101
93
|
end
|
102
94
|
|
103
95
|
def aws_assume_role(role_arn)
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
end
|
96
|
+
begin
|
97
|
+
sts = Aws::STS::Client.new
|
98
|
+
|
99
|
+
role_credentials = Aws::AssumeRoleCredentials.new(
|
100
|
+
client: sts,
|
101
|
+
role_arn: role_arn,
|
102
|
+
role_session_name: 'tfctl',
|
103
|
+
)
|
104
|
+
rescue StandardError => e
|
105
|
+
raise Tfctl::Error, "Error assuming role: #{role_arn}, #{e.message}"
|
106
|
+
end
|
116
107
|
|
117
|
-
|
108
|
+
role_credentials
|
118
109
|
end
|
119
110
|
|
120
111
|
end
|
data/lib/tfctl/config.rb
CHANGED
@@ -18,7 +18,7 @@ module Tfctl
|
|
18
18
|
@config = read_cache(cache_file)
|
19
19
|
else
|
20
20
|
@config = load_config(config_name, yaml_config, aws_org_config)
|
21
|
-
write_cache(
|
21
|
+
write_cache(cache_file)
|
22
22
|
end
|
23
23
|
end
|
24
24
|
|
@@ -30,15 +30,17 @@ module Tfctl
|
|
30
30
|
@config.each(&block)
|
31
31
|
end
|
32
32
|
|
33
|
-
def
|
34
|
-
@config.
|
33
|
+
def key?(key)
|
34
|
+
@config.key?(key)
|
35
35
|
end
|
36
36
|
|
37
|
+
alias has_key? key?
|
38
|
+
|
37
39
|
def to_yaml
|
38
40
|
@config.to_yaml
|
39
41
|
end
|
40
42
|
|
41
|
-
def to_json
|
43
|
+
def to_json(*_args)
|
42
44
|
@config.to_json
|
43
45
|
end
|
44
46
|
|
@@ -52,8 +54,9 @@ module Tfctl
|
|
52
54
|
end
|
53
55
|
|
54
56
|
if output.empty?
|
55
|
-
raise Tfctl::Error
|
57
|
+
raise Tfctl::Error, "Account not found with #{property_name}: #{property_value}"
|
56
58
|
end
|
59
|
+
|
57
60
|
output
|
58
61
|
end
|
59
62
|
|
@@ -65,13 +68,14 @@ module Tfctl
|
|
65
68
|
output << account
|
66
69
|
end
|
67
70
|
rescue RegexpError => e
|
68
|
-
raise Tfctl::Error
|
71
|
+
raise Tfctl::Error, "Regexp: #{e}"
|
69
72
|
end
|
70
73
|
end
|
71
74
|
|
72
75
|
if output.empty?
|
73
|
-
raise Tfctl::Error
|
76
|
+
raise Tfctl::Error, "Account not found with #{property_name} matching regex: #{expr}"
|
74
77
|
end
|
78
|
+
|
75
79
|
output
|
76
80
|
end
|
77
81
|
|
@@ -96,14 +100,14 @@ module Tfctl
|
|
96
100
|
config
|
97
101
|
end
|
98
102
|
|
99
|
-
def write_cache(
|
103
|
+
def write_cache(cache_file)
|
100
104
|
FileUtils.mkdir_p File.dirname(cache_file)
|
101
|
-
File.open(cache_file, 'w') {|f| f.write
|
105
|
+
File.open(cache_file, 'w') { |f| f.write to_yaml }
|
102
106
|
end
|
103
107
|
|
104
108
|
def read_cache(cache_file)
|
105
109
|
unless File.exist?(cache_file)
|
106
|
-
raise Tfctl::Error
|
110
|
+
raise Tfctl::Error, "Cached configuration not found in: #{cache_file}"
|
107
111
|
end
|
108
112
|
|
109
113
|
YAML.load_file(cache_file)
|
@@ -111,16 +115,13 @@ module Tfctl
|
|
111
115
|
|
112
116
|
# Sets :excluded property on any excluded accounts
|
113
117
|
def mark_excluded_accounts(config)
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
end
|
122
|
-
end
|
123
|
-
config
|
118
|
+
return config unless config.key?(:exclude_accounts)
|
119
|
+
|
120
|
+
config[:accounts].each_with_index do |account, idx|
|
121
|
+
config[:accounts][idx][:excluded] = config[:exclude_accounts].include?(account[:name]) ? true : false
|
122
|
+
end
|
123
|
+
|
124
|
+
config
|
124
125
|
end
|
125
126
|
|
126
127
|
def remove_unset_profiles(config)
|
@@ -158,13 +159,13 @@ module Tfctl
|
|
158
159
|
# merge all OU levels settings
|
159
160
|
account_ou_parents.each_with_index do |_, i|
|
160
161
|
account_ou = account_ou_parents[0..i].join('/').to_sym
|
161
|
-
if yaml_config[:organization_units].
|
162
|
+
if yaml_config[:organization_units].key?(account_ou)
|
162
163
|
account_config = account_config.deep_merge(yaml_config[:organization_units][account_ou])
|
163
164
|
end
|
164
165
|
end
|
165
166
|
|
166
167
|
# merge any account overrides
|
167
|
-
if yaml_config[:account_overrides].
|
168
|
+
if yaml_config[:account_overrides].key?(account_name)
|
168
169
|
account_config = account_config.deep_merge(yaml_config[:account_overrides][account_name])
|
169
170
|
end
|
170
171
|
|
data/lib/tfctl/executor.rb
CHANGED
@@ -3,23 +3,18 @@
|
|
3
3
|
require 'open3'
|
4
4
|
require 'fileutils'
|
5
5
|
require 'shellwords'
|
6
|
-
require 'thread'
|
7
6
|
require_relative 'error.rb'
|
8
7
|
|
9
8
|
module Tfctl
|
10
9
|
module Executor
|
11
|
-
|
10
|
+
module_function
|
12
11
|
|
13
12
|
# Execute terraform command
|
14
13
|
def run(account_name:, config_name:, log:, cmd: nil, argv: [], unbuffered: true)
|
15
14
|
|
16
15
|
if cmd.nil?
|
17
|
-
if
|
18
|
-
|
19
|
-
cmd = "#{PROJECT_ROOT}/bin/terraform"
|
20
|
-
else
|
21
|
-
cmd = 'terraform'
|
22
|
-
end
|
16
|
+
# use project terraform binary if available
|
17
|
+
cmd = File.exist?("#{PROJECT_ROOT}/bin/terraform") ? "#{PROJECT_ROOT}/bin/terraform" : 'terraform'
|
23
18
|
end
|
24
19
|
|
25
20
|
path = "#{PROJECT_ROOT}/.tfctl/#{config_name}/#{account_name}"
|
@@ -57,14 +52,14 @@ module Tfctl
|
|
57
52
|
Thread.new do
|
58
53
|
stdout.each do |line|
|
59
54
|
semaphore.synchronize do
|
60
|
-
unbuffered ? log.info("#{account_name}: #{line.chomp}") : output << [
|
55
|
+
unbuffered ? log.info("#{account_name}: #{line.chomp}") : output << ['info', line]
|
61
56
|
end
|
62
57
|
end
|
63
58
|
end
|
64
59
|
Thread.new do
|
65
60
|
stderr.each do |line|
|
66
61
|
semaphore.synchronize do
|
67
|
-
unbuffered ? log.error("#{account_name}: #{line.chomp}") : output << [
|
62
|
+
unbuffered ? log.error("#{account_name}: #{line.chomp}") : output << ['error', line]
|
68
63
|
end
|
69
64
|
end
|
70
65
|
end
|
@@ -79,26 +74,23 @@ module Tfctl
|
|
79
74
|
FileUtils.cd cwd
|
80
75
|
FileUtils.rm_f plan_file if args[0] == 'apply' # tidy up the plan file
|
81
76
|
|
82
|
-
unless status.exitstatus
|
83
|
-
raise Tfctl::Error
|
77
|
+
unless status.exitstatus.zero?
|
78
|
+
raise Tfctl::Error, "#{cmd} failed with exit code: #{status.exitstatus}"
|
84
79
|
end
|
85
80
|
end
|
86
81
|
end
|
87
82
|
|
88
83
|
# Adds plan file to `plan` and `apply` sub commands
|
89
84
|
def plan_file_args(plan_file, subcmd)
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
output = [ "#{plan_file}" ]
|
97
|
-
else
|
98
|
-
raise Tfctl::Error.new "Plan file not found in #{plan_file}. Run plan first."
|
99
|
-
end
|
85
|
+
return ["-out=#{plan_file}"] if subcmd == 'plan'
|
86
|
+
|
87
|
+
if subcmd == 'apply'
|
88
|
+
raise Tfctl::Error, "Plan file not found in #{plan_file}. Run plan first." unless File.exist?(plan_file)
|
89
|
+
|
90
|
+
return [plan_file.to_s]
|
100
91
|
end
|
101
|
-
|
92
|
+
|
93
|
+
return []
|
102
94
|
end
|
103
95
|
end
|
104
96
|
end
|
data/lib/tfctl/generator.rb
CHANGED
@@ -2,11 +2,11 @@
|
|
2
2
|
|
3
3
|
require 'fileutils'
|
4
4
|
|
5
|
-
# Generates Terraform configuration for an account.
|
5
|
+
# Generates top level Terraform configuration for an account.
|
6
6
|
|
7
7
|
module Tfctl
|
8
8
|
module Generator
|
9
|
-
|
9
|
+
module_function
|
10
10
|
|
11
11
|
def write_json_block(path, block)
|
12
12
|
File.open(path, 'w') do |f|
|
@@ -15,7 +15,6 @@ module Tfctl
|
|
15
15
|
end
|
16
16
|
|
17
17
|
def make(
|
18
|
-
terraform_io_org:,
|
19
18
|
account_id:,
|
20
19
|
account_name:,
|
21
20
|
execution_role:,
|
@@ -32,7 +31,7 @@ module Tfctl
|
|
32
31
|
terraform_block = {
|
33
32
|
'terraform' => {
|
34
33
|
'required_version' => tf_version,
|
35
|
-
'backend'
|
34
|
+
'backend' => {
|
36
35
|
's3' => {
|
37
36
|
'bucket' => config[:tf_state_bucket],
|
38
37
|
'key' => "#{account_name}/tfstate",
|
@@ -40,31 +39,31 @@ module Tfctl
|
|
40
39
|
'role_arn' => config[:tf_state_role_arn],
|
41
40
|
'dynamodb_table' => config[:tf_state_dynamodb_table],
|
42
41
|
'encrypt' => 'true',
|
43
|
-
}
|
44
|
-
}
|
45
|
-
}
|
42
|
+
},
|
43
|
+
},
|
44
|
+
},
|
46
45
|
}
|
47
46
|
write_json_block("#{target_dir}/terraform.tf.json", terraform_block)
|
48
47
|
|
49
48
|
provider_block = {
|
50
49
|
'provider' => {
|
51
|
-
'aws'
|
50
|
+
'aws' => {
|
52
51
|
'version' => aws_provider_version,
|
53
52
|
'region' => region,
|
54
53
|
'assume_role' => {
|
55
|
-
'role_arn' => "arn:aws:iam::#{account_id}:role/#{execution_role}"
|
56
|
-
}
|
57
|
-
}
|
58
|
-
}
|
54
|
+
'role_arn' => "arn:aws:iam::#{account_id}:role/#{execution_role}",
|
55
|
+
},
|
56
|
+
},
|
57
|
+
},
|
59
58
|
}
|
60
59
|
write_json_block("#{target_dir}/provider.tf.json", provider_block)
|
61
60
|
|
62
61
|
vars_block = {
|
63
62
|
'variable' => {
|
64
63
|
'config' => {
|
65
|
-
'type' => 'string'
|
66
|
-
}
|
67
|
-
}
|
64
|
+
'type' => 'string',
|
65
|
+
},
|
66
|
+
},
|
68
67
|
}
|
69
68
|
write_json_block("#{target_dir}/vars.tf.json", vars_block)
|
70
69
|
|
@@ -79,13 +78,13 @@ module Tfctl
|
|
79
78
|
profile_block = {
|
80
79
|
'module' => {
|
81
80
|
profile => {
|
82
|
-
'source'
|
83
|
-
'config'
|
81
|
+
'source' => "../../../profiles/#{profile}",
|
82
|
+
'config' => '${var.config}',
|
84
83
|
'providers' => {
|
85
|
-
|
86
|
-
}
|
87
|
-
}
|
88
|
-
}
|
84
|
+
'aws' => 'aws',
|
85
|
+
},
|
86
|
+
},
|
87
|
+
},
|
89
88
|
}
|
90
89
|
|
91
90
|
write_json_block("#{target_dir}/profile_#{profile}.tf.json", profile_block)
|
data/lib/tfctl/logger.rb
CHANGED
@@ -10,7 +10,7 @@ module Tfctl
|
|
10
10
|
|
11
11
|
self.level = log_level
|
12
12
|
|
13
|
-
@outlog.formatter = proc do |severity,
|
13
|
+
@outlog.formatter = proc do |severity, _datetime, _progname, msg|
|
14
14
|
# "#{datetime.iso8601} #{severity.downcase}: #{msg}\n"
|
15
15
|
"#{severity.downcase}: #{msg}\n"
|
16
16
|
end
|
@@ -24,11 +24,25 @@ module Tfctl
|
|
24
24
|
@outlog.level
|
25
25
|
end
|
26
26
|
|
27
|
-
def debug(msg)
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
def
|
27
|
+
def debug(msg)
|
28
|
+
log(:debug, msg)
|
29
|
+
end
|
30
|
+
|
31
|
+
def info(msg)
|
32
|
+
log(:info, msg)
|
33
|
+
end
|
34
|
+
|
35
|
+
def warn(msg)
|
36
|
+
log(:warn, msg)
|
37
|
+
end
|
38
|
+
|
39
|
+
def error(msg)
|
40
|
+
log(:error, msg)
|
41
|
+
end
|
42
|
+
|
43
|
+
def fatal(msg)
|
44
|
+
log(:fatal, msg)
|
45
|
+
end
|
32
46
|
|
33
47
|
def log(level, msg)
|
34
48
|
@outlog.send(level, msg)
|
data/lib/tfctl/version.rb
CHANGED
data/tfctl.gemspec
CHANGED
@@ -1,29 +1,34 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
-
|
2
|
+
|
3
|
+
$LOAD_PATH << File.expand_path('lib', __dir__)
|
3
4
|
require 'tfctl/version'
|
4
5
|
|
5
6
|
Gem::Specification.new do |spec|
|
6
7
|
spec.name = 'tfctl'
|
7
8
|
spec.version = Tfctl::VERSION
|
8
9
|
spec.authors = [
|
9
|
-
'Andrew Wasilczuk'
|
10
|
+
'Andrew Wasilczuk',
|
10
11
|
]
|
11
12
|
spec.email = [
|
12
|
-
'akw@scalefactory.com'
|
13
|
+
'akw@scalefactory.com',
|
13
14
|
]
|
14
15
|
spec.summary = 'Terraform wrapper for managing multi-account AWS infrastructures'
|
15
16
|
spec.homepage = 'https://github.com/scalefactory/tfctl'
|
16
|
-
spec.license =
|
17
|
-
spec.files = `git ls-files -z`.split("\x0").reject
|
17
|
+
spec.license = 'MIT'
|
18
|
+
spec.files = `git ls-files -z`.split("\x0").reject do |f|
|
18
19
|
f.match(%r{^(test|spec|features)/})
|
19
|
-
|
20
|
-
spec.bindir =
|
20
|
+
end
|
21
|
+
spec.bindir = 'bin'
|
21
22
|
spec.executables = spec.files.grep(%r{^bin/tfctl}) { |f| File.basename(f) }
|
22
23
|
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
23
|
-
spec.require_paths = [
|
24
|
+
spec.require_paths = ['lib']
|
24
25
|
|
26
|
+
# Think when adding new dependencies. Is it really necessary?
|
27
|
+
# "The things you own end up owning you" etc.
|
25
28
|
spec.add_dependency 'aws-sdk-organizations', '~> 1.13'
|
26
29
|
spec.add_dependency 'parallel', '~> 1.17'
|
30
|
+
spec.add_dependency 'terminal-table', '~> 1.8'
|
27
31
|
|
28
32
|
spec.add_development_dependency 'rspec', '~> 3.8'
|
33
|
+
spec.add_development_dependency 'rubocop', '~> 0.76'
|
29
34
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: tfctl
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0
|
4
|
+
version: 0.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Andrew Wasilczuk
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2019-11-
|
11
|
+
date: 2019-11-09 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: aws-sdk-organizations
|
@@ -38,6 +38,20 @@ dependencies:
|
|
38
38
|
- - "~>"
|
39
39
|
- !ruby/object:Gem::Version
|
40
40
|
version: '1.17'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: terminal-table
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '1.8'
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '1.8'
|
41
55
|
- !ruby/object:Gem::Dependency
|
42
56
|
name: rspec
|
43
57
|
requirement: !ruby/object:Gem::Requirement
|
@@ -52,6 +66,20 @@ dependencies:
|
|
52
66
|
- - "~>"
|
53
67
|
- !ruby/object:Gem::Version
|
54
68
|
version: '3.8'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: rubocop
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0.76'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0.76'
|
55
83
|
description:
|
56
84
|
email:
|
57
85
|
- akw@scalefactory.com
|
@@ -62,6 +90,7 @@ extra_rdoc_files: []
|
|
62
90
|
files:
|
63
91
|
- ".gitignore"
|
64
92
|
- ".rspec"
|
93
|
+
- ".rubocop.yml"
|
65
94
|
- ".travis.yml"
|
66
95
|
- CHANGELOG.adoc
|
67
96
|
- Gemfile
|
@@ -112,7 +141,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
112
141
|
- !ruby/object:Gem::Version
|
113
142
|
version: '0'
|
114
143
|
requirements: []
|
115
|
-
|
144
|
+
rubyforge_project:
|
145
|
+
rubygems_version: 2.7.7
|
116
146
|
signing_key:
|
117
147
|
specification_version: 4
|
118
148
|
summary: Terraform wrapper for managing multi-account AWS infrastructures
|