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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0e1075714676ca848752725f2d98c38240e5a9ee2bc901b582ffba6edd46fc0f
4
- data.tar.gz: b48bc64d5a9aaae352538ed88b648b20e2b2c68c59bd0c1b3b5c8757e3c421b9
3
+ metadata.gz: cac739f38f0484a3f0a8373224183f0705c79372962c58711f82719978d86fbe
4
+ data.tar.gz: 8ed33267e070125bb03e1591075642163c367f8d5ab1d4ee4040c2afc97a11d8
5
5
  SHA512:
6
- metadata.gz: b87ecf1c66f1528bf98134d631aef3c8264326bbd0e399eb769860ad853d24ebc59339ba6df16be41b6d728c4fd63df035a210a1ca9d09fd17960de2b4d66fa1
7
- data.tar.gz: 657f44ce06c1ac667525a80009f73e88fbf9754f32f5703faad8fbfe1b6355fd6796690c8b0583af549fef8d75de318d1dc98f56aa80644b78ff2a068e30bfa7
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
@@ -1,5 +1,9 @@
1
1
  = Changelog
2
2
 
3
+ == 0.1.0
4
+
5
+ * FEATURE: Added `-l` switch to list discovered accounts.
6
+
3
7
  == 0.0.2
4
8
 
5
9
  * BUGFIX: Fixed an exception when `exclude_accounts` is not set.
data/Gemfile CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  source 'https://rubygems.org'
2
4
 
3
5
  gemspec
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
- Clone this repository and run:
45
+ To install the latest release from RubyGems run:
43
46
 
44
47
  ----
45
- make install
48
+ gem install tfctl
46
49
  ----
47
50
 
48
- This will build a ruby gem and install it.
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
- gem 'tfctl', git: 'https://github.com/scalefactory/tfctl'
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
- Run Terraform init accross all accounts:
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
- :account => nil,
20
- :ou => nil,
21
- :all => nil,
22
- :show_config => false,
23
- :config_file => nil,
24
- :unbuffered => false,
25
- :debug => false,
26
- :use_cache => false
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', "Target a specific AWS account") do |o|
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', "Target accounts in an Organization Unit (uses regex matching)") do |o|
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', "Target all accounts") do
38
+ opts.on('--all', 'Target all accounts') do
37
39
  options[:all] = true
38
40
  end
39
- opts.on('-c', '--config-file=config', "Path to config file") do |o|
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', "Display configuration") do
44
+ opts.on('-s', '--show-config', 'Display configuration') do
43
45
  options[:show_config] = true
44
46
  end
45
- opts.on('-x', '--use-cache', "Use cached AWS organization data") do
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', "Disable buffering of Terraform output") do
53
+ opts.on('-u', '--unbuffered', 'Disable buffering of Terraform output') do
49
54
  options[:unbuffered] = true
50
55
  end
51
- opts.on('-d', '--debug', "Turn on debug messages") do
56
+ opts.on('-d', '--debug', 'Turn on debug messages') do
52
57
  options[:debug] = true
53
58
  end
54
- opts.on('-v', '--version', "Show version") do
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.exists? options[:config_file]
75
+ unless File.exist? options[:config_file]
71
76
  raise OptionParser::InvalidOption,
72
- "Config file not found in: #{options[:config_file]}"
77
+ "Config file not found in: #{options[:config_file]}"
73
78
  end
74
79
 
75
- unless File.exists? options[:config_file]
76
- raise OptionParser::InvalidOption, "Config file #{options[:config_file]} not found."
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 = [:account, :ou, :all]
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 == nil
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
- "Too many target options set: #{targets_set.join(', ')}. Only one can be specified."
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, "Please specify target"
97
+ raise OptionParser::InvalidOption, 'Please specify target'
93
98
  end
94
-
95
99
  rescue OptionParser::InvalidOption, OptionParser::MissingArgument
96
- $stderr.puts $!.to_s
97
- $stderr.puts optparse
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: config,
118
- terraform_io_org: config[:terraform_io_org],
119
- account_id: account[:id],
120
- account_name: account[:name],
121
- profiles: account[:profiles],
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] ? log_level = Logger::DEBUG : log_level = Logger::INFO
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.load(File.read(options[:config_file]))
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
- config_name: config_name,
163
- yaml_config: yaml_config,
164
- aws_org_config: aws_org_accounts,
165
- use_cache: options[:use_cache])
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
- # Run targets
175
+ # Find target accounts
174
176
 
175
177
  if options[:account]
176
- account = config.find_accounts(:name, options[:account]).first
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
- Parallel.each(accounts, in_processes: 8) do |ac|
188
- run_account(config, ac, options, ARGV, log)
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
- log.info 'Done'
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 accounts under the Core OU
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 ::Hash
5
+ class Hash
6
6
  def deep_merge(second)
7
- merger = proc { |key, v1, v2|
8
- Hash === v1 && Hash === v2 ? v1.merge(v2, &merger) :
9
- Array === v1 && Array === v2 ? v1 | v2 :
10
- [:undefined, nil, :nil].include?(v2) ? v1 : v2
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
- self.merge(second.to_h, &merger)
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
- result.keys.each do |key|
20
- result[key.to_sym] = symbolize_names!(result.delete(key))
21
- end
25
+ result.keys.each do |key|
26
+ result[key.to_sym] = symbolize_names!(result.delete(key))
27
+ end
22
28
  when Array
23
- result.map! { |r| symbolize_names!(r) }
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
- region: 'us-east-1',
12
- # Assume role in primary account to read AWS organization API
13
- credentials: aws_assume_role(role_arn)
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 = { :accounts => [] }
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
- if aws_ou_ids.has_key?(ou_path)
26
- parent_id = aws_ou_ids[ou_path]
27
- else
28
- raise Tfctl::Error.new "Error: OU: #{ou_path}, does not exists in AWS organization"
29
- end
30
-
31
- @aws_org_client.list_accounts_for_parent({ parent_id: parent_id }).accounts.each do |account|
32
- if account.status == 'ACTIVE'
33
-
34
- output[:accounts] << {
35
- :name => account.name,
36
- :id => account.id,
37
- :arn => account.arn,
38
- :email => account.email,
39
- :ou_path => ou_path.to_s,
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({ :root => root_ou_id })
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: parent_id,
78
- }).children.each do |child|
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
- }).organizational_unit
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
- if parent_name == :root
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
- begin
105
- sts = Aws::STS::Client.new()
106
-
107
- role_credentials = Aws::AssumeRoleCredentials.new(
108
- client: sts,
109
- role_arn: role_arn,
110
- role_session_name: 'tfctl'
111
- )
112
- rescue StandardError => e
113
- raise Tfctl::Error.new("Error assuming role: #{role_arn}, #{e.message}")
114
- exit 1
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
- role_credentials
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(@config, cache_file)
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 has_key?(k)
34
- @config.has_key?(k)
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.new "Account not found with #{property_name}: #{property_value}"
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.new "Regexp: #{e}"
71
+ raise Tfctl::Error, "Regexp: #{e}"
69
72
  end
70
73
  end
71
74
 
72
75
  if output.empty?
73
- raise Tfctl::Error.new "Account not found with #{property_name} matching regex: #{expr}"
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(config, cache_file)
103
+ def write_cache(cache_file)
100
104
  FileUtils.mkdir_p File.dirname(cache_file)
101
- File.open(cache_file, 'w') {|f| f.write self.to_yaml }
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.new("Cached configuration not found in: #{cache_file}")
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
- return config unless config.has_key?(:exclude_accounts)
115
-
116
- config[:accounts].each_with_index do |account, idx|
117
- if config[:exclude_accounts].include?(account[:name])
118
- config[:accounts][idx][:excluded] = true
119
- else
120
- config[:accounts][idx][:excluded] = false
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].has_key?(account_ou)
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].has_key?(account_name)
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
 
@@ -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
- extend self
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 File.exists?("#{PROJECT_ROOT}/bin/terraform")
18
- # use embedded terraform binary
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 << [ 'info', line ]
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 << [ 'error', line ]
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 == 0
83
- raise Tfctl::Error.new "#{cmd} failed with exit code: #{status.exitstatus}"
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
- output = []
91
- if subcmd == 'plan'
92
- output = [ "-out=#{plan_file}" ]
93
-
94
- elsif subcmd == 'apply'
95
- if File.exists?(plan_file)
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
- output
92
+
93
+ return []
102
94
  end
103
95
  end
104
96
  end
@@ -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
- extend self
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' => "../../../profiles/#{profile}",
83
- 'config' => '${var.config}',
81
+ 'source' => "../../../profiles/#{profile}",
82
+ 'config' => '${var.config}',
84
83
  'providers' => {
85
- 'aws' => 'aws'
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, datetime, progname, msg|
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); log(:debug, msg); end
28
- def info(msg); log(:info, msg); end
29
- def warn(msg); log(:warn, msg); end
30
- def error(msg); log(:error, msg); end
31
- def fatal(msg); log(:fatal, msg); end
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Tfctl
4
- VERSION = '0.0.2'
4
+ VERSION = '0.1.0'
5
5
  end
data/tfctl.gemspec CHANGED
@@ -1,29 +1,34 @@
1
1
  # frozen_string_literal: true
2
- $LOAD_PATH << File.expand_path("../lib", __FILE__)
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 = "MIT"
17
- spec.files = `git ls-files -z`.split("\x0").reject { |f|
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 = "bin"
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 = ["lib"]
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.2
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-08 00:00:00.000000000 Z
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
- rubygems_version: 3.0.3
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