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.
- checksums.yaml +4 -4
- checksums.yaml.gz.sig +0 -0
- data.tar.gz.sig +0 -0
- data/lib/geoengineer/cli/geo_cli.rb +16 -5
- data/lib/geoengineer/cli/status_command.rb +38 -46
- data/lib/geoengineer/cli/terraform_commands.rb +8 -2
- data/lib/geoengineer/environment.rb +5 -1
- data/lib/geoengineer/resource.rb +54 -35
- data/lib/geoengineer/resources/aws_cloudwatch_metric_alarm.rb +48 -0
- data/lib/geoengineer/resources/aws_iam_account_password_policy.rb +32 -0
- data/lib/geoengineer/resources/aws_iam_instance_profile.rb +28 -0
- data/lib/geoengineer/resources/aws_iam_policy.rb +5 -5
- data/lib/geoengineer/resources/aws_iam_user.rb +3 -3
- data/lib/geoengineer/resources/aws_kms_key.rb +27 -0
- data/lib/geoengineer/resources/aws_lambda_function.rb +3 -0
- data/lib/geoengineer/resources/aws_lambda_permission.rb +24 -18
- data/lib/geoengineer/resources/aws_ses_receipt_rule.rb +2 -2
- data/lib/geoengineer/resources/aws_ses_receipt_rule_set.rb +2 -2
- data/lib/geoengineer/resources/aws_sns_topic.rb +3 -3
- data/lib/geoengineer/resources/aws_sns_topic_subscription.rb +17 -6
- data/lib/geoengineer/resources/aws_sqs_queue.rb +3 -3
- data/lib/geoengineer/template.rb +3 -0
- data/lib/geoengineer/utils/aws_clients.rb +7 -0
- data/lib/geoengineer/version.rb +1 -1
- data/spec/resource_spec.rb +119 -18
- data/spec/resources/aws_cloudwatch_metric_alarm_spec.rb +38 -0
- data/spec/resources/aws_iam_account_password_policy_spec.rb +51 -0
- data/spec/resources/aws_iam_instance_profile_spec.rb +40 -0
- data/spec/resources/aws_kinesis_stream_spec.rb +1 -0
- data/spec/resources/aws_kms_key_spec.rb +44 -0
- data/spec/resources/aws_lambda_permission_spec.rb +0 -38
- metadata +17 -5
- metadata.gz.sig +0 -0
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 10a3ba58afd64f71c7acc870e588f452f26f544a
|
4
|
+
data.tar.gz: 50072402a6cd1b74baa75df96161f2a7d98d56b7
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
-
|
203
|
-
|
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
|
14
|
-
|
15
|
-
|
16
|
-
|
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
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
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
|
-
|
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 -
|
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 -
|
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 =
|
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,
|
data/lib/geoengineer/resource.rb
CHANGED
@@ -17,7 +17,7 @@ class GeoEngineer::Resource
|
|
17
17
|
|
18
18
|
attr_reader :type, :id
|
19
19
|
|
20
|
-
before :validation, :
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
169
|
-
|
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 #{
|
171
|
+
throw "NOT IMPLEMENTED ERROR for #{name}"
|
176
172
|
end
|
177
173
|
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
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
|
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
|
220
|
-
return unless
|
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
|
-
|
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
|
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
|
-
|
245
|
-
|
246
|
-
|
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
|
-
|
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
|