geoengineer 0.1.2 → 0.1.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (33) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data.tar.gz.sig +0 -0
  4. data/lib/geoengineer/cli/geo_cli.rb +16 -5
  5. data/lib/geoengineer/cli/status_command.rb +38 -46
  6. data/lib/geoengineer/cli/terraform_commands.rb +8 -2
  7. data/lib/geoengineer/environment.rb +5 -1
  8. data/lib/geoengineer/resource.rb +54 -35
  9. data/lib/geoengineer/resources/aws_cloudwatch_metric_alarm.rb +48 -0
  10. data/lib/geoengineer/resources/aws_iam_account_password_policy.rb +32 -0
  11. data/lib/geoengineer/resources/aws_iam_instance_profile.rb +28 -0
  12. data/lib/geoengineer/resources/aws_iam_policy.rb +5 -5
  13. data/lib/geoengineer/resources/aws_iam_user.rb +3 -3
  14. data/lib/geoengineer/resources/aws_kms_key.rb +27 -0
  15. data/lib/geoengineer/resources/aws_lambda_function.rb +3 -0
  16. data/lib/geoengineer/resources/aws_lambda_permission.rb +24 -18
  17. data/lib/geoengineer/resources/aws_ses_receipt_rule.rb +2 -2
  18. data/lib/geoengineer/resources/aws_ses_receipt_rule_set.rb +2 -2
  19. data/lib/geoengineer/resources/aws_sns_topic.rb +3 -3
  20. data/lib/geoengineer/resources/aws_sns_topic_subscription.rb +17 -6
  21. data/lib/geoengineer/resources/aws_sqs_queue.rb +3 -3
  22. data/lib/geoengineer/template.rb +3 -0
  23. data/lib/geoengineer/utils/aws_clients.rb +7 -0
  24. data/lib/geoengineer/version.rb +1 -1
  25. data/spec/resource_spec.rb +119 -18
  26. data/spec/resources/aws_cloudwatch_metric_alarm_spec.rb +38 -0
  27. data/spec/resources/aws_iam_account_password_policy_spec.rb +51 -0
  28. data/spec/resources/aws_iam_instance_profile_spec.rb +40 -0
  29. data/spec/resources/aws_kinesis_stream_spec.rb +1 -0
  30. data/spec/resources/aws_kms_key_spec.rb +44 -0
  31. data/spec/resources/aws_lambda_permission_spec.rb +0 -38
  32. metadata +17 -5
  33. metadata.gz.sig +0 -0
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: dda3981b643cb0887fcdb1e794abfa4b8c6ac9c0
4
- data.tar.gz: 68d052b3c9cd6a7a3f77571837cfda41b1413161
3
+ metadata.gz: 10a3ba58afd64f71c7acc870e588f452f26f544a
4
+ data.tar.gz: 50072402a6cd1b74baa75df96161f2a7d98d56b7
5
5
  SHA512:
6
- metadata.gz: 8b2bae296535675da928d1decd4e9d96f448876642e953f40d50e6f4ed919ddb3cd6bebc89380a19dddf27305735d8ed2d212af29485baf42ccd582a54751432
7
- data.tar.gz: 7e9eb34de6c0d0cb88d2f182ed1e618570404d30bc6c84f97c3a04bd13544e2191ba4bb82cc29e1e333f554881701f1630702072a60dea4ae005ca04d303a434
6
+ metadata.gz: 9999badc1ff21e9043a4f753e62a271ab98a6d09c137b948ced6802c61c32b7e68a2321aa95d65c2ab2c77f0d6d44b103df6a13567c13a0599a66dbee34db97d
7
+ data.tar.gz: e5e1b196d5582b70dc792c1a163ea0bf451285048c62f5a7df8de50ddab2c9e0326e7bd47106f347057396212bdb613ab9d7e14e41dc7029919400dc41d059c3
checksums.yaml.gz.sig CHANGED
Binary file
data.tar.gz.sig CHANGED
Binary file
@@ -2,7 +2,6 @@ require_relative '../../geoengineer'
2
2
  require 'open3'
3
3
  require 'commander'
4
4
  require 'colorize'
5
- require 'terminal-table'
6
5
  require 'fileutils'
7
6
  require 'json'
8
7
  require 'singleton'
@@ -32,6 +31,7 @@ class GeoCLI
32
31
  include Singleton
33
32
  include StatusCommand
34
33
  include TerraformCommands
34
+ include HasLifecycle
35
35
 
36
36
  attr_accessor :environment, :env_name
37
37
 
@@ -186,6 +186,13 @@ class GeoCLI
186
186
  terraform_version.exitstatus.zero?
187
187
  end
188
188
 
189
+ def add_commands
190
+ plan_cmd
191
+ apply_cmd
192
+ graph_cmd
193
+ status_cmd
194
+ end
195
+
189
196
  def run
190
197
  program :name, 'GeoEngineer'
191
198
  program :version, GeoEngineer::VERSION
@@ -198,11 +205,15 @@ class GeoCLI
198
205
  # global_options
199
206
  global_options
200
207
 
208
+ # Require any patches to the way geo works
209
+ if File.file?("#{Dir.pwd}/.geo.rb")
210
+ require_from_pwd '.geo'
211
+ puts "Loaded patches from .geo.rb" if @verbose
212
+ end
213
+
201
214
  # Add commands
202
- plan_cmd
203
- apply_cmd
204
- graph_cmd
205
- status_cmd
215
+ add_commands
216
+ execute_lifecycle(:after, :add_commands)
206
217
 
207
218
  # Execute the CLI
208
219
  run!
@@ -10,33 +10,10 @@ module GeoCLI::StatusCommand
10
10
  }
11
11
  end
12
12
 
13
- def status_resource_rows(reses)
14
- rows = []
15
- rows << :separator
16
- rows << ['TerraformID', 'GeoID']
17
- rows << :separator
18
- reses.each do |sg|
19
- g_id = sg._geo_id
20
- g_id = "<<" if sg._terraform_id == sg._geo_id
21
- rows << [sg._terraform_id, g_id]
22
- end
23
- rows << :separator
24
- rows
25
- end
26
-
27
- def status_type_rows(type, codified, uncodified, stats)
28
- rows = []
29
-
30
- # Codified resources
31
- rows << [{ value: "### CODIFIED #{type} ###".colorize(:green), colspan: 2, alignment: :left }]
32
- rows.concat status_resource_rows(codified)
33
-
34
- # Uncodified resources
35
- rows << [{ value: "### UNCODIFIED #{type} ###".colorize(:red), colspan: 2, alignment: :left }]
36
- rows.concat status_resource_rows(uncodified)
37
-
38
- rows.concat status_rows(stats)
39
- puts Terminal::Table.new({ rows: rows })
13
+ def resource_id_array(resources)
14
+ resources
15
+ .select { |r| !r.attributes.empty? }
16
+ .map { |r| r._geo_id || r._terraform_id }
40
17
  end
41
18
 
42
19
  def status_types(options)
@@ -62,35 +39,50 @@ module GeoCLI::StatusCommand
62
39
  total: 0
63
40
  }
64
41
  type_stats.each do |type, stats|
65
- totals[:codified] += stats[:codified]
66
- totals[:uncodified] += stats[:uncodified]
67
- totals[:total] += stats[:total]
42
+ totals[:codified] += stats[:stats][:codified]
43
+ totals[:uncodified] += stats[:stats][:uncodified]
44
+ totals[:total] += stats[:stats][:total]
68
45
  end
69
46
  totals[:percent] = (100.0 * totals[:codified]) / totals[:total]
70
47
  totals
71
48
  end
72
49
 
73
- def status_rows(stats)
74
- rows = []
75
- rows << ['CODIFIED'.colorize(:green), stats[:codified]]
76
- rows << ['UNCODIFIED'.colorize(:red), stats[:uncodified]]
77
- rows << ['TOTAL'.colorize(:blue), stats[:total]]
78
- rows << ['PERCENT CODIFIED'.colorize({ mode: :bold }), format('%.2f%', stats[:percent])]
79
- rows
50
+ def report_json(type_stats, status)
51
+ status[:resources] = {}
52
+ type_stats.each do |type, resources|
53
+ status[:resources][type] = {}
54
+ status[:resources][type][:uncodified] = resource_id_array(resources[:uncodified])
55
+ status[:resources][type][:codified] = resource_id_array(resources[:codified])
56
+ end
57
+ status
58
+ end
59
+
60
+ def type_stats(options)
61
+ type_stats = {}
62
+ status_types(options).each do |type|
63
+ type_stats[type] = {}
64
+ type_stats[type][:codified] = @environment.codified_resources(type)
65
+ type_stats[type][:uncodified] = @environment.uncodified_resources(type)
66
+ type_stats[type][:stats] = calculate_type_status(
67
+ type_stats[type][:codified],
68
+ type_stats[type][:uncodified]
69
+ )
70
+ end
71
+ type_stats
72
+ end
73
+
74
+ def only_codified(status)
75
+ status[:resources]
76
+ .select { |t, r| r[:uncodified].any? }
77
+ .each { |t, r| r.delete(:codified) }
80
78
  end
81
79
 
82
80
  def status_action
83
81
  lambda do |args, options|
84
- type_stats = {}
85
- status_types(options).each do |type|
86
- codified = @environment.codified_resources(type)
87
- uncodified = @environment.uncodified_resources(type)
88
- type_stats[type] = calculate_type_status(codified, uncodified)
89
- status_type_rows(type, codified, uncodified, type_stats[type]) if @verbose
90
- end
91
-
82
+ type_stats = type_stats(options)
92
83
  status = calculate_status(type_stats)
93
- puts Terminal::Table.new({ rows: status_rows(status) }) if @verbose
84
+ status = report_json(type_stats, status)
85
+ status[:resources] = only_codified(status) unless @verbose
94
86
  puts JSON.pretty_generate(status)
95
87
  end
96
88
  end
@@ -15,10 +15,15 @@ module GeoCLI::TerraformCommands
15
15
  }
16
16
  end
17
17
 
18
+ def terraform_parallelism
19
+ Parallel.processor_count * 3 # Determined through trial/error
20
+ end
21
+
18
22
  def terraform_plan
19
23
  plan_commands = [
20
24
  "cd #{@tmpdir}",
21
- "terraform plan -state=#{@terraform_state_file} -out=#{@plan_file} #{@no_color}"
25
+ "terraform plan -parallelism=#{terraform_parallelism}" \
26
+ " -state=#{@terraform_state_file} -out=#{@plan_file} #{@no_color}"
22
27
  ]
23
28
  shell_exec(plan_commands.join(" && "), true)
24
29
  end
@@ -26,7 +31,8 @@ module GeoCLI::TerraformCommands
26
31
  def terraform_apply
27
32
  apply_commands = [
28
33
  "cd #{@tmpdir}",
29
- "terraform apply -state=#{@terraform_state_file} #{@plan_file} #{@no_color}"
34
+ "terraform apply -parallelism=#{terraform_parallelism}" \
35
+ " -state=#{@terraform_state_file} #{@plan_file} #{@no_color}"
30
36
  ]
31
37
  shell_exec(apply_commands.join(" && "), true)
32
38
  end
@@ -1,3 +1,4 @@
1
+ require 'parallel'
1
2
 
2
3
  ########################################################################
3
4
  # An Environment is a group of projects, resources and attributes,
@@ -8,6 +9,7 @@
8
9
  ########################################################################
9
10
  class GeoEngineer::Environment
10
11
  include HasAttributes
12
+ include HasSubResources
11
13
  include HasResources
12
14
  include HasProjects
13
15
  include HasTemplates
@@ -157,7 +159,9 @@ class GeoEngineer::Environment
157
159
  def to_terraform_state
158
160
  reses = all_resources.select(&:_terraform_id) # _terraform_id must not be nil
159
161
 
160
- reses = reses.map { |r| { "#{r.type}.#{r.id}" => r.to_terraform_state() } }.reduce({}, :merge)
162
+ reses = Parallel.map(reses, { in_threads: Parallel.processor_count }) do |r|
163
+ { "#{r.type}.#{r.id}" => r.to_terraform_state() }
164
+ end.reduce({}, :merge)
161
165
 
162
166
  {
163
167
  version: 1,
@@ -17,7 +17,7 @@ class GeoEngineer::Resource
17
17
 
18
18
  attr_reader :type, :id
19
19
 
20
- before :validation, :merge_project_tags
20
+ before :validation, :merge_parent_tags
21
21
 
22
22
  validate -> { validate_required_attributes([:_geo_id]) }
23
23
 
@@ -113,7 +113,6 @@ class GeoEngineer::Resource
113
113
  raw = File.open(path, "rb").read
114
114
  interpolated = ERB.new(raw).result(binding_obj)
115
115
  escaped = interpolated.gsub("$", "$$")
116
-
117
116
  # normalize JSON to prevent terraform from e.g. newlines as legitimate changes
118
117
  normalized = _normalize_json(escaped)
119
118
 
@@ -121,8 +120,7 @@ class GeoEngineer::Resource
121
120
  end
122
121
 
123
122
  def _normalize_json(json)
124
- h = JSON.parse(json)
125
- h.to_json
123
+ JSON.parse(json).to_json
126
124
  end
127
125
 
128
126
  # REMOTE METHODS
@@ -137,11 +135,9 @@ class GeoEngineer::Resource
137
135
  return GeoEngineer::Resource.build(remote_resource_params) if find_remote_as_individual?
138
136
 
139
137
  matches = matched_remote_resource
138
+ throw "ERROR:\"#{type}.#{id}\" has #{matches.length} remote resources" if matches.length > 1
140
139
 
141
- return matches.first if matches.length == 1
142
- return nil if matches.empty?
143
-
144
- throw "ERROR:\"#{self.type}.#{self.id}\" has #{matches.length} remote resources"
140
+ matches.first
145
141
  end
146
142
 
147
143
  # By default, remote resources are bulk-retrieved. In order to fetch a remote resource as an
@@ -159,27 +155,47 @@ class GeoEngineer::Resource
159
155
  end
160
156
 
161
157
  def matched_remote_resource
162
- aws_resources = self.class.fetch_remote_resources()
163
- aws_resources.select { |r| r._geo_id == self._geo_id }
158
+ self.class.fetch_remote_resources.select { |r| r._geo_id == _geo_id }
164
159
  end
165
160
 
166
161
  def self.fetch_remote_resources
167
162
  return @_rr_cache if @_rr_cache
168
- resource_hashes = _fetch_remote_resources()
169
- @_rr_cache = resource_hashes.map { |res_hash| GeoEngineer::Resource.build(res_hash) }
163
+ @_rr_cache = _fetch_remote_resources
164
+ .reject { |resource| _ignore_remote_resource?(resource) }
165
+ .map { |resource| GeoEngineer::Resource.build(resource) }
170
166
  end
171
167
 
172
168
  # This method must be implemented for each resource type
173
169
  # it must return a list of hashes with at least the key
174
170
  def self._fetch_remote_resources
175
- throw "NOT IMPLEMENTED ERROR for #{self.name}"
171
+ throw "NOT IMPLEMENTED ERROR for #{name}"
176
172
  end
177
173
 
178
- def self.build(resource_hash)
179
- GeoEngineer::Resource.new(self.type_from_class_name, resource_hash['_geo_id']) {
180
- resource_hash.each do |k, v|
181
- self[k] = v
174
+ # This method allows you to specify certain remote resources that for whatever reason,
175
+ # cannot or should not be codified. It expects a list of `_geo_ids`, and can be overriden
176
+ # in child classes.
177
+ def self._resources_to_ignore
178
+ []
179
+ end
180
+
181
+ def self._ignore_remote_resource?(resource)
182
+ _resources_to_ignore.include?(_deep_symbolize_keys(resource)[:_geo_id])
183
+ end
184
+
185
+ def self._deep_symbolize_keys(obj)
186
+ case obj
187
+ when Hash then
188
+ obj.each_with_object({}) do |(key, value), hash|
189
+ hash[key.to_sym] = _deep_symbolize_keys(value)
182
190
  end
191
+ when Array then obj.map { |value| _deep_symbolize_keys(value) }
192
+ else obj
193
+ end
194
+ end
195
+
196
+ def self.build(resource_hash)
197
+ GeoEngineer::Resource.new(type_from_class_name, resource_hash['_geo_id']) {
198
+ resource_hash.each { |k, v| self[k] = v }
183
199
  }
184
200
  end
185
201
 
@@ -196,8 +212,7 @@ class GeoEngineer::Resource
196
212
  def short_id
197
213
  si = id.to_s.tr('-', "_")
198
214
  si = si.gsub(project.full_id_name, '') if project
199
- si = si.gsub('__', '_').gsub(/^_|_$/, '')
200
- si
215
+ si.gsub('__', '_').gsub(/^_|_$/, '')
201
216
  end
202
217
 
203
218
  def short_name
@@ -216,19 +231,26 @@ class GeoEngineer::Resource
216
231
  tags {} unless tags
217
232
  end
218
233
 
219
- def merge_project_tags
220
- return unless self.project && self.project.tags && self.support_tags?
234
+ def merge_parent_tags
235
+ return unless support_tags?
221
236
 
237
+ %i(project environment).each do |source|
238
+ parent = send(source)
239
+ next unless parent
240
+ next unless parent.methods.include?(:attributes)
241
+ next unless parent&.tags
242
+ merge_tags(source)
243
+ end
244
+ end
245
+
246
+ def merge_tags(source)
222
247
  setup_tags_if_needed
223
248
 
224
- self
225
- .project
249
+ send(source)
226
250
  .all_tags
227
251
  .map(&:attributes)
228
252
  .reduce({}, :merge)
229
253
  .each { |key, value| tags.attributes[key] ||= value }
230
-
231
- tags
232
254
  end
233
255
 
234
256
  # VALIDATION METHODS
@@ -237,17 +259,15 @@ class GeoEngineer::Resource
237
259
  end
238
260
 
239
261
  def validate_required_subresource(subresource)
240
- "Subresource '#{subresource}'' required #{for_resource}" if self.send(subresource.to_sym).nil?
262
+ "Subresource '#{subresource}'' required #{for_resource}" if send(subresource.to_sym).nil?
241
263
  end
242
264
 
243
265
  def validate_subresource_required_attributes(subresource, keys)
244
- errs = []
245
- self.send("all_#{subresource}".to_sym).each do |sr|
246
- keys.each do |key|
247
- errs << "#{key} attribute on subresource #{subresource} nil #{for_resource}" if sr[key].nil?
266
+ send("all_#{subresource}".to_sym).map do |sr|
267
+ keys.map do |key|
268
+ "#{key} attribute on subresource #{subresource} nil #{for_resource}" if sr[key].nil?
248
269
  end
249
- end
250
- errs
270
+ end.flatten.compact
251
271
  end
252
272
 
253
273
  def validate_has_tag(tag)
@@ -260,10 +280,9 @@ class GeoEngineer::Resource
260
280
  # CLASS METHODS
261
281
  def self.type_from_class_name
262
282
  # from http://stackoverflow.com/questions/1509915/converting-camel-case-to-underscore-case-in-ruby
263
- self.name.split('::').last
283
+ name.split('::').last
264
284
  .gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
265
285
  .gsub(/([a-z\d])([A-Z])/, '\1_\2')
266
- .tr("-", "_")
267
- .downcase
286
+ .tr("-", "_").downcase
268
287
  end
269
288
  end
@@ -0,0 +1,48 @@
1
+ ########################################################################
2
+ # AwsCloudwatchMetricAlarm is the +aws_cloudwatch_metric_alarm+ terrform resource,
3
+ #
4
+ # {https://www.terraform.io/docs/providers/aws/r/aws_cloudwatch_metric_alarm.html Terraform Docs}
5
+ ########################################################################
6
+ class GeoEngineer::Resources::AwsCloudwatchMetricAlarm < GeoEngineer::Resource
7
+ validate -> {
8
+ validate_required_attributes([
9
+ :alarm_name,
10
+ :comparison_operator,
11
+ :evaluation_periods,
12
+ :metric_name,
13
+ :namespace,
14
+ :period,
15
+ :threshold
16
+ ])
17
+ }
18
+
19
+ after :initialize, -> { _terraform_id -> { NullObject.maybe(remote_resource)._terraform_id } }
20
+ after :initialize, -> { _geo_id -> { alarm_name } }
21
+
22
+ def support_tags?
23
+ false
24
+ end
25
+
26
+ def self._fetch_remote_resources
27
+ _get_all_alarms.map { |alarm|
28
+ {
29
+ _terraform_id: alarm[:alarm_name],
30
+ _geo_id: alarm[:alarm_name],
31
+ alarm_name: alarm[:alarm_name]
32
+ }
33
+ }
34
+ end
35
+
36
+ def self._get_all_alarms
37
+ alarm_page = AwsClients.cloudwatch.describe_alarms({ max_records: 100 })
38
+ alarms = alarm_page.metric_alarms.map(&:to_h)
39
+ while alarm_page.next_token
40
+ alarm_page = AwsClients.cloudwatch.describe_alarms({
41
+ max_records: 100,
42
+ next_token: alarm_page.next_token
43
+ })
44
+ alarms.concat alarm_page.metric_alarms.map(&:to_h)
45
+ end
46
+ alarms
47
+ end
48
+ end