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 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