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
@@ -0,0 +1,32 @@
1
+ ########################################################################
2
+ # AwsIamPasswordPolicy +aws_iam_password_policy+ terrform resource,
3
+ #
4
+ # {https://www.terraform.io/docs/providers/aws/r/iam_account_password_policy.html Terraform Docs}
5
+ ########################################################################
6
+ class GeoEngineer::Resources::AwsIamAccountPasswordPolicy < GeoEngineer::Resource
7
+ # There can only be a single IAM account password policy - use this constant as the Geo ID
8
+ SINGLETON_ID = 'GEO_SINGLETON_RESOURCE_ID'.freeze
9
+
10
+ validate -> { validate_required_attributes([:allow_users_to_change_password]) }
11
+
12
+ after :initialize, -> {
13
+ _terraform_id -> { NullObject.maybe(remote_resource)._terraform_id }
14
+ }
15
+
16
+ after :initialize, -> { _geo_id -> { SINGLETON_ID } }
17
+
18
+ def support_tags?
19
+ false
20
+ end
21
+
22
+ def find_remote_as_individual?
23
+ true
24
+ end
25
+
26
+ def remote_resource_params
27
+ password_policy = AwsClients.iam.get_account_password_policy.password_policy.to_h
28
+ password_policy.merge({ _geo_id: SINGLETON_ID, _terraform_id: SINGLETON_ID })
29
+ rescue Aws::IAM::Errors::NoSuchEntity
30
+ {}
31
+ end
32
+ end
@@ -0,0 +1,28 @@
1
+ ########################################################################
2
+ # AwsIamInstanceProfile +aws_iam_instance_profile+ terrform resource,
3
+ #
4
+ # {https://www.terraform.io/docs/providers/aws/r/iam_instance_profile.html Terraform Docs}
5
+ ########################################################################
6
+ class GeoEngineer::Resources::AwsIamInstanceProfile < GeoEngineer::Resource
7
+ validate -> { validate_required_attributes([:name, :roles]) }
8
+
9
+ before :validation, -> { policy_arn _policy.to_ref(:arn) if _policy }
10
+
11
+ after :initialize, -> { _terraform_id -> { NullObject.maybe(remote_resource)._terraform_id } }
12
+ after :initialize, -> { _geo_id -> { name.to_s } }
13
+
14
+ def support_tags?
15
+ false
16
+ end
17
+
18
+ def self._fetch_remote_resources
19
+ profiles = AwsClients.iam.list_instance_profiles['instance_profiles'].map(&:to_h)
20
+ profiles.map do |p|
21
+ {
22
+ name: p[:instance_profile_name],
23
+ _geo_id: p[:instance_profile_name],
24
+ _terraform_id: p[:instance_profile_name]
25
+ }
26
+ end
27
+ end
28
+ end
@@ -51,11 +51,11 @@ class GeoEngineer::Resources::AwsIamPolicy < GeoEngineer::Resource
51
51
  def self._fetch_remote_resources
52
52
  _all_remote_policies.map(&:to_h).map do |policy|
53
53
  {
54
- '_terraform_id' => policy[:arn],
55
- '_geo_id' => policy[:policy_name],
56
- 'arn' => policy[:arn],
57
- 'default_version_id' => policy[:default_version_id],
58
- 'name' => policy[:policy_name]
54
+ _terraform_id: policy[:arn],
55
+ _geo_id: policy[:policy_name],
56
+ arn: policy[:arn],
57
+ default_version_id: policy[:default_version_id],
58
+ name: policy[:policy_name]
59
59
  }
60
60
  end
61
61
  end
@@ -33,9 +33,9 @@ class GeoEngineer::Resources::AwsIamUser < GeoEngineer::Resource
33
33
  def self._fetch_remote_resources
34
34
  _all_remote_users.map do |user|
35
35
  {
36
- '_terraform_id' => user[:user_name],
37
- '_geo_id' => user[:user_name],
38
- 'name' => user[:user_name]
36
+ _terraform_id: user[:user_name],
37
+ _geo_id: user[:user_name],
38
+ name: user[:user_name]
39
39
  }
40
40
  end
41
41
  end
@@ -0,0 +1,27 @@
1
+ ########################################################################
2
+ # AwsKmsKey is the +aws_kms_key+ terrform resource,
3
+ #
4
+ # {https://www.terraform.io/docs/providers/aws/r/kms_key.html}
5
+ ########################################################################
6
+ class GeoEngineer::Resources::AwsKmsKey < GeoEngineer::Resource
7
+ validate -> { validate_required_attributes([:description]) }
8
+
9
+ after :initialize, -> { _terraform_id -> { NullObject.maybe(remote_resource)._terraform_id } }
10
+ after :initialize, -> { _geo_id -> { description } }
11
+
12
+ def self._fetch_remote_resources
13
+ keys = AwsClients.kms.list_keys[:keys].map do |i|
14
+ AwsClients.kms.describe_key({ key_id: i.key_id }).key_metadata.to_h
15
+ end
16
+
17
+ keys.map do |k|
18
+ k[:_terraform_id] = k[:key_id]
19
+ k[:_geo_id] = k[:description]
20
+ k
21
+ end
22
+ end
23
+
24
+ def support_tags?
25
+ false
26
+ end
27
+ end
@@ -26,6 +26,9 @@ class GeoEngineer::Resources::AwsLambdaFunction < GeoEngineer::Resource
26
26
  's3_bucket' => (s3_bucket || ""),
27
27
  's3_key' => (s3_key || "")
28
28
  }
29
+
30
+ tfstate[:primary][:attributes]['filename'] = filename if filename
31
+
29
32
  tfstate
30
33
  end
31
34
 
@@ -40,38 +40,26 @@ class GeoEngineer::Resources::AwsLambdaPermission < GeoEngineer::Resource
40
40
  nil
41
41
  end
42
42
 
43
- def self._deep_symbolize_keys(obj)
44
- if obj.is_a?(Hash)
45
- obj.each_with_object({}) do |(key, value), hash|
46
- hash[key.to_sym] = _deep_symbolize_keys(value)
47
- end
48
- elsif obj.is_a?(Array)
49
- obj.map { |value| _deep_symbolize_keys(value) }
50
- else
51
- obj
52
- end
53
- end
54
-
55
43
  def self._create_permission(function)
56
44
  policy = function[:policy]
57
45
  policy[:Statement].map do |statement|
58
- # Note that the keys for a statement objection are all CamelCased
46
+ # Note that the keys for a statement object are all CamelCased
59
47
  # Whereas most other keys in this repo are snake_cased
60
48
  statement.merge(
61
49
  {
62
50
  _terraform_id: statement[:Sid],
63
51
  function_name: function[:function_name],
64
- function_version: function[:version]
52
+ function_version: function[:version],
53
+ qualifer: function[:qualifer] || "$LATEST",
54
+ statement_id: statement[:Sid]
65
55
  }
66
56
  )
67
57
  end
68
58
  end
69
59
 
70
60
  # Right now, this only fetches policies for the $LATEST version
71
- # If we want to support fetching the permissions for all of the aliases as well,
72
- # We'll need to add another call per function, bring total calls to 2N+1...
73
- # Same deal if we need to support older versions...
74
- # (excluding any extra calls for pagination). Less than ideal...
61
+ # If you want to fetch the policy for a version other than $LATEST
62
+ # set `find_remote_as_individual?` to `true` for that resource
75
63
  def self._fetch_remote_resources
76
64
  _fetch_functions
77
65
  .map { |function| _fetch_policy(function) }
@@ -80,4 +68,22 @@ class GeoEngineer::Resources::AwsLambdaPermission < GeoEngineer::Resource
80
68
  .flatten
81
69
  .compact
82
70
  end
71
+
72
+ def remote_resource_params
73
+ params = { function_name: function_name }
74
+ params[:qualifier] = qualifier if qualifier
75
+
76
+ begin
77
+ policy = _fetch_policy(params)[:policy]
78
+ return {} if policy.nil?
79
+ rescue Aws::Lambda::Errors::ResourceNotFoundException
80
+ return {}
81
+ end
82
+
83
+ permission = _create_permission(policy).find do |statement|
84
+ statement[:_terraform_id] == statement_id
85
+ end
86
+
87
+ permission || {}
88
+ end
83
89
  end
@@ -30,8 +30,8 @@ class GeoEngineer::Resources::AwsSesReceiptRule < GeoEngineer::Resource
30
30
  def self._fetch_remote_resources
31
31
  AwsClients.ses.describe_active_receipt_rule_set.rules.map(&:to_h).map do |rule|
32
32
  {
33
- '_terraform_id' => rule[:name],
34
- '_geo_id' => rule[:name]
33
+ _terraform_id: rule[:name],
34
+ _geo_id: rule[:name]
35
35
  }
36
36
  end
37
37
  end
@@ -20,8 +20,8 @@ class GeoEngineer::Resources::AwsSesReceiptRuleSet < GeoEngineer::Resource
20
20
  def self._fetch_remote_resources
21
21
  AwsClients.ses.list_receipt_rule_sets.rule_sets.map(&:to_h).map do |rule_set|
22
22
  {
23
- '_terraform_id' => rule_set[:name],
24
- '_geo_id' => rule_set[:name]
23
+ _terraform_id: rule_set[:name],
24
+ _geo_id: rule_set[:name]
25
25
  }
26
26
  end
27
27
  end
@@ -19,9 +19,9 @@ class GeoEngineer::Resources::AwsSnsTopic < GeoEngineer::Resource
19
19
  def self._fetch_remote_resources
20
20
  AwsClients.sns.list_topics.topics.map(&:to_h).map do |topic|
21
21
  {
22
- '_terraform_id' => topic[:topic_arn],
23
- '_geo_id' => topic[:topic_arn],
24
- 'name' => topic[:topic_arn].split(':').last
22
+ _terraform_id: topic[:topic_arn],
23
+ _geo_id: topic[:topic_arn],
24
+ name: topic[:topic_arn].split(':').last
25
25
  }
26
26
  end
27
27
  end
@@ -20,7 +20,8 @@ class GeoEngineer::Resources::AwsSnsTopicSubscription < GeoEngineer::Resource
20
20
  'endpoint' => endpoint,
21
21
  'protocol' => protocol,
22
22
  'confirmation_timeout_in_minutes' => "1",
23
- 'endpoint_auto_confirms' => "false"
23
+ 'endpoint_auto_confirms' => "false",
24
+ 'raw_message_delivery' => "false"
24
25
  }
25
26
  tfstate
26
27
  end
@@ -30,13 +31,23 @@ class GeoEngineer::Resources::AwsSnsTopicSubscription < GeoEngineer::Resource
30
31
  end
31
32
 
32
33
  def self._fetch_remote_resources
33
- AwsClients.sns.list_subscriptions.subscriptions.map(&:to_h).map do |subscription|
34
+ _get_all_subscriptions.map do |subscription|
34
35
  {
35
- '_terraform_id' => subscription[:subscription_arn],
36
- '_geo_id' => "#{subscription[:topic_arn]}::" \
37
- "#{subscription[:protocol]}::" \
38
- "#{subscription[:endpoint]}"
36
+ _terraform_id: subscription[:subscription_arn],
37
+ _geo_id: "#{subscription[:topic_arn]}::" \
38
+ "#{subscription[:protocol]}::" \
39
+ "#{subscription[:endpoint]}"
39
40
  }
40
41
  end
41
42
  end
43
+
44
+ def self._get_all_subscriptions
45
+ subs_page = AwsClients.sns.list_subscriptions
46
+ subs = subs_page.subscriptions.map(&:to_h)
47
+ while subs_page.next_token
48
+ subs_page = AwsClients.sns.list_subscriptions({ next_token: subs_page.next_token })
49
+ subs.concat subs_page.subscriptions.map(&:to_h)
50
+ end
51
+ subs
52
+ end
42
53
  end
@@ -28,9 +28,9 @@ class GeoEngineer::Resources::AwsSqsQueue < GeoEngineer::Resource
28
28
  def self._fetch_remote_resources
29
29
  AwsClients.sqs.list_queues['queue_urls'].map do |queue|
30
30
  {
31
- '_terraform_id' => queue,
32
- '_geo_id' => queue,
33
- 'name' => URI.parse(queue).path.split('/').last
31
+ _terraform_id: queue,
32
+ _geo_id: queue,
33
+ name: URI.parse(queue).path.split('/').last
34
34
  }
35
35
  end
36
36
  end
@@ -5,8 +5,11 @@ class GeoEngineer::Template
5
5
  include HasAttributes
6
6
  include HasResources
7
7
 
8
+ attr_accessor :name, :parameters
9
+
8
10
  def initialize(name, parent, parameters = {})
9
11
  @name = name
12
+ @parameters = parameters
10
13
  case parent
11
14
  when GeoEngineer::Project then add_project_attributes(parent)
12
15
  when GeoEngineer::Environment then add_env_attributes(parent)
@@ -12,6 +12,9 @@ class AwsClients
12
12
  end
13
13
 
14
14
  # Clients
15
+ def self.cloudwatch
16
+ @aws_cloudwatch ||= Aws::CloudWatch::Client.new({ stub_responses: stubbed? })
17
+ end
15
18
 
16
19
  def self.cloudwatchevents
17
20
  @aws_cloudwatchevents ||= Aws::CloudWatchEvents::Client.new({ stub_responses: stubbed? })
@@ -80,4 +83,8 @@ class AwsClients
80
83
  def self.cloudtrail
81
84
  @aws_cloudtrail ||= Aws::CloudTrail::Client.new({ stub_responses: stubbed? })
82
85
  end
86
+
87
+ def self.kms
88
+ @aws_kms ||= Aws::KMS::Client.new({ stub_responses: stubbed? })
89
+ end
83
90
  end
@@ -1,3 +1,3 @@
1
1
  module GeoEngineer
2
- VERSION = '0.1.2'.freeze
2
+ VERSION = '0.1.3'.freeze
3
3
  end
@@ -6,7 +6,9 @@ class GeoEngineer::RemoteResources < GeoEngineer::Resource
6
6
  end
7
7
  end
8
8
 
9
- describe("GeoEngineer::Resource") do
9
+ describe GeoEngineer::Resource do
10
+ let(:env) { GeoEngineer::Environment.new("testing") }
11
+
10
12
  describe '#remote_resource' do
11
13
  it 'should return a list of resources' do
12
14
  rem_res = GeoEngineer::RemoteResources.new('rem', 'id') {
@@ -99,6 +101,24 @@ describe("GeoEngineer::Resource") do
99
101
  end
100
102
  end
101
103
 
104
+ describe '#_resources_to_ignore' do
105
+ it 'lets you ignore certain resources' do
106
+ class GeoEngineer::IgnorableResources < GeoEngineer::Resource
107
+ def self._fetch_remote_resources
108
+ [{ _geo_id: "geoid1" }, { _geo_id: "geoid2" }]
109
+ end
110
+
111
+ def self._resources_to_ignore
112
+ ["geoid1"]
113
+ end
114
+ end
115
+
116
+ resources = GeoEngineer::IgnorableResources.fetch_remote_resources()
117
+ expect(resources.length).to eq 1
118
+ expect(resources[0]._geo_id).to eq "geoid2"
119
+ end
120
+ end
121
+
102
122
  describe '#validate_required_subresource' do
103
123
  it 'should return errors if it does not have a tag' do
104
124
  class GeoEngineer::HasSRAttrResource < GeoEngineer::Resource
@@ -168,57 +188,100 @@ describe("GeoEngineer::Resource") do
168
188
  end
169
189
 
170
190
  describe '#validate_tag_merge' do
171
- it 'should combine resource and project tags' do
172
- project = GeoEngineer::Project.new('org', 'project_name', 'test') {
191
+ it 'combines resource and parent tags' do
192
+ environment = GeoEngineer::Environment.new('test') {
193
+ tags {
194
+ a '1'
195
+ }
196
+ }
197
+ project = GeoEngineer::Project.new('org', 'project_name', environment) {
198
+ tags {
199
+ b '2'
200
+ }
201
+ }
202
+ resource = project.resource('type', '1') {
203
+ tags {
204
+ c '3'
205
+ }
206
+ }
207
+ resource.merge_parent_tags
208
+ expect(resource.tags.attributes).to eq({ 'a' => '1', 'b' => '2', 'c' => '3' })
209
+ end
210
+
211
+ it 'works if just project is present' do
212
+ project = GeoEngineer::Project.new('org', 'project_name', nil) {
173
213
  tags {
174
214
  a '1'
175
215
  }
176
216
  }
177
217
  resource = project.resource('type', '1') {
178
218
  tags {
179
- d '4'
219
+ b '2'
180
220
  }
181
221
  }
182
- resource.merge_project_tags
183
- expect(resource.tags.attributes).to eq({ 'a' => '1', 'd' => '4' })
222
+ resource.merge_parent_tags
223
+ expect(resource.tags.attributes).to eq({ 'a' => '1', 'b' => '2' })
184
224
  end
185
225
 
186
- it 'should give priority to resource tags' do
187
- project = GeoEngineer::Project.new('org', 'project_name', 'test') {
226
+ it 'works if just environment is present' do
227
+ environment = GeoEngineer::Environment.new('test') {
188
228
  tags {
189
- a 'project_value'
229
+ a '1'
230
+ }
231
+ }
232
+ resource = environment.resource('type', '1') {
233
+ tags {
234
+ b '2'
235
+ }
236
+ }
237
+ resource.merge_parent_tags
238
+ expect(resource.tags.attributes).to eq({ 'a' => '1', 'b' => '2' })
239
+ end
240
+
241
+ it 'uses priority: resource > project > environment' do
242
+ environment = GeoEngineer::Environment.new('test') {
243
+ tags {
244
+ a '1'
245
+ }
246
+ }
247
+ project = GeoEngineer::Project.new('org', 'project_name', environment) {
248
+ tags {
249
+ a '2'
250
+ b '1'
190
251
  }
191
252
  }
192
253
  resource = project.resource('type', '1') {
193
254
  tags {
194
- a 'resource_value'
255
+ a '3'
256
+ b '2'
257
+ c '1'
195
258
  }
196
259
  }
197
- resource.merge_project_tags
198
- expect(resource.tags.attributes).to eq({ 'a' => 'resource_value' })
260
+ resource.merge_parent_tags
261
+ expect(resource.tags.attributes).to eq({ 'a' => '3', 'b' => '2', 'c' => '1' })
199
262
  end
200
263
 
201
- it 'should return project tags if there are no resource tags' do
202
- project = GeoEngineer::Project.new('org', 'project_name', 'test') {
264
+ it 'returns project tags if there are no resource tags' do
265
+ project = GeoEngineer::Project.new('org', 'project_name', env) {
203
266
  tags {
204
267
  a '1'
205
268
  b '2'
206
269
  }
207
270
  }
208
271
  resource = project.resource('type', '1') {}
209
- resource.merge_project_tags
272
+ resource.merge_parent_tags
210
273
  expect(resource.tags.attributes).to eq({ 'a' => '1', 'b' => '2' })
211
274
  end
212
275
 
213
- it 'should return resource tags if there are no project tags' do
214
- project = GeoEngineer::Project.new('org', 'project_name', 'test') {}
276
+ it 'returns resource tags if there are no project tags' do
277
+ project = GeoEngineer::Project.new('org', 'project_name', env) {}
215
278
  resource = project.resource('type', '1') {
216
279
  tags {
217
280
  c '3'
218
281
  d '4'
219
282
  }
220
283
  }
221
- resource.merge_project_tags
284
+ resource.merge_parent_tags
222
285
  expect(resource.tags.attributes).to eq({ 'c' => '3', 'd' => '4' })
223
286
  end
224
287
  end
@@ -261,4 +324,42 @@ describe("GeoEngineer::Resource") do
261
324
  end
262
325
  end
263
326
  end
327
+
328
+ describe '#_deep_symbolize_keys' do
329
+ let(:simple_obj) { JSON.parse({ foo: "bar", baz: "qux" }.to_json) }
330
+ let(:complex_obj) do
331
+ JSON.parse(
332
+ {
333
+ foo: {
334
+ bar: {
335
+ baz: [
336
+ { qux: "quack" }
337
+ ]
338
+ }
339
+ },
340
+ bar: [
341
+ { foo: "bar" },
342
+ nil,
343
+ [{ baz: "qux" }],
344
+ 1,
345
+ "baz"
346
+ ]
347
+ }.to_json
348
+ )
349
+ end
350
+
351
+ it "converts top level keys to symbols" do
352
+ expect(simple_obj.keys.include?(:foo)).to eq(false)
353
+ expect(simple_obj.keys.include?("foo")).to eq(true)
354
+ converted = described_class._deep_symbolize_keys(simple_obj)
355
+ expect(converted.keys.include?(:foo)).to eq(true)
356
+ expect(converted.keys.include?("foo")).to eq(false)
357
+ end
358
+
359
+ it "converts deeply nested keys to symbols" do
360
+ converted = described_class._deep_symbolize_keys(complex_obj)
361
+ expect(converted[:foo][:bar][:baz].first[:qux]).to eq("quack")
362
+ expect(converted[:bar].first[:foo]).to eq("bar")
363
+ end
364
+ end
264
365
  end