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