geoengineer 0.1.2 → 0.1.3
Sign up to get free protection for your applications and to get access to all the features.
- 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
|