geoengineer 0.1.2 → 0.1.3

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