tfctl 0.0.2 → 0.1.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/.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
|