cloud-mu 3.2.0 → 3.3.0

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 (91) hide show
  1. checksums.yaml +4 -4
  2. data/Dockerfile +1 -1
  3. data/bin/mu-adopt +12 -1
  4. data/bin/mu-load-config.rb +2 -1
  5. data/bin/mu-run-tests +14 -2
  6. data/cloud-mu.gemspec +3 -3
  7. data/modules/mu.rb +2 -2
  8. data/modules/mu/adoption.rb +5 -5
  9. data/modules/mu/cleanup.rb +47 -25
  10. data/modules/mu/cloud.rb +29 -1
  11. data/modules/mu/cloud/dnszone.rb +0 -2
  12. data/modules/mu/cloud/resource_base.rb +9 -3
  13. data/modules/mu/cloud/wrappers.rb +4 -0
  14. data/modules/mu/config.rb +1 -1
  15. data/modules/mu/config/bucket.rb +31 -2
  16. data/modules/mu/config/cache_cluster.rb +1 -1
  17. data/modules/mu/config/cdn.rb +100 -0
  18. data/modules/mu/config/container_cluster.rb +1 -1
  19. data/modules/mu/config/database.rb +1 -1
  20. data/modules/mu/config/dnszone.rb +4 -3
  21. data/modules/mu/config/endpoint.rb +1 -0
  22. data/modules/mu/config/function.rb +16 -7
  23. data/modules/mu/config/job.rb +89 -0
  24. data/modules/mu/config/notifier.rb +7 -18
  25. data/modules/mu/config/ref.rb +53 -7
  26. data/modules/mu/config/server.rb +1 -1
  27. data/modules/mu/config/vpc.rb +1 -0
  28. data/modules/mu/defaults/AWS.yaml +26 -26
  29. data/modules/mu/deploy.rb +13 -0
  30. data/modules/mu/master.rb +21 -0
  31. data/modules/mu/mommacat.rb +1 -0
  32. data/modules/mu/mommacat/daemon.rb +13 -7
  33. data/modules/mu/providers/aws.rb +115 -16
  34. data/modules/mu/providers/aws/alarm.rb +2 -2
  35. data/modules/mu/providers/aws/bucket.rb +274 -40
  36. data/modules/mu/providers/aws/cache_cluster.rb +4 -4
  37. data/modules/mu/providers/aws/cdn.rb +782 -0
  38. data/modules/mu/providers/aws/collection.rb +2 -2
  39. data/modules/mu/providers/aws/container_cluster.rb +57 -37
  40. data/modules/mu/providers/aws/database.rb +11 -11
  41. data/modules/mu/providers/aws/dnszone.rb +24 -7
  42. data/modules/mu/providers/aws/endpoint.rb +535 -50
  43. data/modules/mu/providers/aws/firewall_rule.rb +6 -3
  44. data/modules/mu/providers/aws/folder.rb +1 -1
  45. data/modules/mu/providers/aws/function.rb +288 -125
  46. data/modules/mu/providers/aws/group.rb +9 -7
  47. data/modules/mu/providers/aws/habitat.rb +2 -2
  48. data/modules/mu/providers/aws/job.rb +466 -0
  49. data/modules/mu/providers/aws/loadbalancer.rb +9 -8
  50. data/modules/mu/providers/aws/log.rb +3 -3
  51. data/modules/mu/providers/aws/msg_queue.rb +12 -3
  52. data/modules/mu/providers/aws/nosqldb.rb +96 -5
  53. data/modules/mu/providers/aws/notifier.rb +135 -63
  54. data/modules/mu/providers/aws/role.rb +51 -37
  55. data/modules/mu/providers/aws/search_domain.rb +165 -29
  56. data/modules/mu/providers/aws/server.rb +12 -9
  57. data/modules/mu/providers/aws/server_pool.rb +26 -13
  58. data/modules/mu/providers/aws/storage_pool.rb +2 -2
  59. data/modules/mu/providers/aws/user.rb +4 -4
  60. data/modules/mu/providers/aws/userdata/linux.erb +5 -4
  61. data/modules/mu/providers/aws/vpc.rb +3 -3
  62. data/modules/mu/providers/azure/server.rb +2 -1
  63. data/modules/mu/providers/google.rb +1 -0
  64. data/modules/mu/providers/google/bucket.rb +1 -1
  65. data/modules/mu/providers/google/container_cluster.rb +1 -1
  66. data/modules/mu/providers/google/database.rb +1 -1
  67. data/modules/mu/providers/google/firewall_rule.rb +1 -1
  68. data/modules/mu/providers/google/folder.rb +1 -1
  69. data/modules/mu/providers/google/function.rb +1 -1
  70. data/modules/mu/providers/google/group.rb +1 -1
  71. data/modules/mu/providers/google/habitat.rb +1 -1
  72. data/modules/mu/providers/google/loadbalancer.rb +1 -1
  73. data/modules/mu/providers/google/role.rb +4 -2
  74. data/modules/mu/providers/google/server.rb +1 -1
  75. data/modules/mu/providers/google/server_pool.rb +1 -1
  76. data/modules/mu/providers/google/user.rb +1 -1
  77. data/modules/mu/providers/google/vpc.rb +1 -1
  78. data/modules/tests/aws-jobs-functions.yaml +46 -0
  79. data/modules/tests/centos6.yaml +4 -0
  80. data/modules/tests/centos7.yaml +4 -0
  81. data/modules/tests/ecs.yaml +2 -2
  82. data/modules/tests/eks.yaml +1 -1
  83. data/modules/tests/functions/node-function/lambda_function.js +10 -0
  84. data/modules/tests/functions/python-function/lambda_function.py +12 -0
  85. data/modules/tests/microservice_app.yaml +288 -0
  86. data/modules/tests/rds.yaml +5 -5
  87. data/modules/tests/regrooms/rds.yaml +5 -5
  88. data/modules/tests/server-with-scrub-muisms.yaml +1 -1
  89. data/modules/tests/super_complex_bok.yml +2 -2
  90. data/modules/tests/super_simple_bok.yml +2 -2
  91. metadata +12 -4
@@ -583,6 +583,7 @@ module MU
583
583
  # Wrapper for cloud_desc method that deals with elb vs. elb2 resources.
584
584
  def cloud_desc(use_cache: true)
585
585
  return @cloud_desc_cache if @cloud_desc_cache and use_cache
586
+ return nil if !@cloud_id
586
587
  if @config['classic']
587
588
  @cloud_desc_cache = MU::Cloud::AWS.elb(region: @config['region'], credentials: @config['credentials']).describe_load_balancers(
588
589
  load_balancer_names: [@cloud_id]
@@ -671,8 +672,8 @@ module MU
671
672
  # @param ignoremaster [Boolean]: If true, will remove resources not flagged as originating from this Mu server
672
673
  # @param region [String]: The cloud provider region
673
674
  # @return [void]
674
- def self.cleanup(noop: false, ignoremaster: false, region: MU.curRegion, credentials: nil, flags: {})
675
- if (MU.deploy_id.nil? or MU.deploy_id.empty?) and (!flags or !flags["vpc_id"])
675
+ def self.cleanup(noop: false, deploy_id: MU.deploy_id, ignoremaster: false, region: MU.curRegion, credentials: nil, flags: {})
676
+ if (deploy_id.nil? or deploy_id.empty?) and (!flags or !flags["vpc_id"])
676
677
  raise MuError, "Can't touch ELBs without MU-ID or vpc_id flag"
677
678
  end
678
679
 
@@ -682,7 +683,7 @@ module MU
682
683
  # @param region [String]: The cloud provider region
683
684
  # @param ignoremaster [Boolean]: Whether to ignore the MU-MASTER-IP tag
684
685
  # @param classic [Boolean]: Whether to look for a classic ELB instead of an ALB (ELB2)
685
- def self.checkForTagMatch(arn, region, ignoremaster, credentials, classic = false)
686
+ def self.checkForTagMatch(arn, region, ignoremaster, credentials, classic = false, deploy_id: MU.deploy_id)
686
687
  tags = []
687
688
  if classic
688
689
  tags = MU::Cloud::AWS.elb(credentials: credentials, region: region).describe_tags(
@@ -699,7 +700,7 @@ module MU
699
700
  if !tags.nil?
700
701
  tags.each { |tag|
701
702
  saw_tags << tag.key
702
- muid_match = true if tag.key == "MU-ID" and tag.value == MU.deploy_id
703
+ muid_match = true if tag.key == "MU-ID" and tag.value == deploy_id
703
704
  mumaster_match = true if tag.key == "MU-MASTER-IP" and tag.value == MU.mu_public_ip
704
705
  }
705
706
  end
@@ -725,9 +726,9 @@ module MU
725
726
  matched = true if lb.vpc_id == flags['vpc_id']
726
727
  else
727
728
  if classic
728
- matched = self.checkForTagMatch(lb.load_balancer_name, region, ignoremaster, credentials, classic)
729
+ matched = self.checkForTagMatch(lb.load_balancer_name, region, ignoremaster, credentials, classic, deploy_id: deploy_id)
729
730
  else
730
- matched = self.checkForTagMatch(lb.load_balancer_arn, region, ignoremaster, credentials, classic)
731
+ matched = self.checkForTagMatch(lb.load_balancer_arn, region, ignoremaster, credentials, classic, deploy_id: deploy_id)
731
732
  end
732
733
  end
733
734
  if matched
@@ -773,7 +774,7 @@ module MU
773
774
 
774
775
 
775
776
  tgs.each { |tg|
776
- if self.checkForTagMatch(tg.target_group_arn, region, ignoremaster, credentials)
777
+ if self.checkForTagMatch(tg.target_group_arn, region, ignoremaster, credentials, deploy_id: deploy_id)
777
778
  MU.log "Removing Load Balancer Target Group #{tg.target_group_name}"
778
779
  retries = 0
779
780
  begin
@@ -837,7 +838,7 @@ module MU
837
838
  (!listener["ssl_certificate_id"].nil? and !listener["ssl_certificate_id"].empty?)
838
839
  if lb['cloud'] != "CloudFormation" # XXX or maybe do this anyway?
839
840
  begin
840
- listener["ssl_certificate_id"] = MU::Cloud::AWS.findSSLCertificate(name: listener["ssl_certificate_name"].to_s, id: listener["ssl_certificate_id"].to_s, region: lb['region'])
841
+ listener["ssl_certificate_id"] = MU::Cloud::AWS.findSSLCertificate(name: listener["ssl_certificate_name"].to_s, id: listener["ssl_certificate_id"].to_s, region: lb['region']).first
841
842
  rescue MuError
842
843
  ok = false
843
844
  next
@@ -202,14 +202,14 @@ module MU
202
202
  # @param ignoremaster [Boolean]: If true, will remove resources not flagged as originating from this Mu server
203
203
  # @param region [String]: The cloud provider region
204
204
  # @return [void]
205
- def self.cleanup(noop: false, ignoremaster: false, region: MU.curRegion, credentials: nil, flags: {})
205
+ def self.cleanup(noop: false, deploy_id: MU.deploy_id, ignoremaster: false, region: MU.curRegion, credentials: nil, flags: {})
206
206
  MU.log "AWS::Log.cleanup: need to support flags['known']", MU::DEBUG, details: flags
207
207
  MU.log "Placeholder: AWS Log artifacts do not support tags, so ignoremaster cleanup flag has no effect", MU::DEBUG, details: ignoremaster
208
208
 
209
209
  log_groups = self.find(credentials: credentials, region: region).values
210
210
  if !log_groups.empty?
211
211
  log_groups.each{ |lg|
212
- if lg.log_group_name.match(MU.deploy_id)
212
+ if lg.log_group_name.match(deploy_id)
213
213
  log_streams = MU::Cloud::AWS.cloudwatchlogs(credentials: credentials, region: region).describe_log_streams(log_group_name: lg.log_group_name).log_streams
214
214
  if !log_streams.empty?
215
215
  log_streams.each{ |ls|
@@ -232,7 +232,7 @@ module MU
232
232
 
233
233
  # unless noop
234
234
  # MU::Cloud::AWS.iam(credentials: credentials).list_roles.roles.each{ |role|
235
- # match_string = "#{MU.deploy_id}.*CloudTrail"
235
+ # match_string = "#{deploy_id}.*CloudTrail"
236
236
  # Maybe we should have a more generic way to delete IAM profiles and policies. The call itself should be moved from MU::Cloud.resourceClass("AWS", "Server").
237
237
  # MU::Cloud.resourceClass("AWS", "Server").removeIAMProfile(role.role_name) if role.role_name.match(match_string)
238
238
  # }
@@ -80,6 +80,7 @@ module MU
80
80
  # @return [Hash]: AWS doesn't return anything but the SQS URL, so supplement with attributes
81
81
  def cloud_desc(use_cache: true)
82
82
  return @cloud_desc_cache if @cloud_desc_cache and use_cache
83
+ return nil if !@cloud_id
83
84
 
84
85
  if !@cloud_id
85
86
  resp = MU::Cloud::AWS.sqs(region: @config['region'], credentials: @config['credentials']).list_queues(
@@ -133,12 +134,12 @@ module MU
133
134
  # @param ignoremaster [Boolean]: If true, will remove resources not flagged as originating from this Mu server
134
135
  # @param region [String]: The cloud provider region
135
136
  # @return [void]
136
- def self.cleanup(noop: false, ignoremaster: false, region: MU.curRegion, credentials: nil, flags: {})
137
+ def self.cleanup(noop: false, deploy_id: MU.deploy_id, ignoremaster: false, region: MU.curRegion, credentials: nil, flags: {})
137
138
  MU.log "AWS::MsgQueue.cleanup: need to support flags['known']", MU::DEBUG, details: flags
138
139
  MU.log "Placeholder: AWS MsgQueue artifacts do not support tags, so ignoremaster cleanup flag has no effect", MU::DEBUG, details: ignoremaster
139
140
 
140
141
  resp = MU::Cloud::AWS.sqs(credentials: credentials, region: region).list_queues(
141
- queue_name_prefix: MU.deploy_id
142
+ queue_name_prefix: deploy_id
142
143
  )
143
144
  if resp and resp.queue_urls
144
145
  threads = []
@@ -194,7 +195,15 @@ module MU
194
195
 
195
196
  # Go fetch its attributes
196
197
  fetch = if args[:cloud_id]
197
- [args[:cloud_id]]
198
+ if args[:cloud_id] !~ /^https?:\/\//
199
+ [begin
200
+ MU::Cloud::AWS.sqs(region: args[:region], credentials: args[:credentials]).get_queue_url(queue_name: args[:cloud_id]).queue_url
201
+ rescue Aws::SQS::Errors::NonExistentQueue
202
+ return found
203
+ end]
204
+ else
205
+ [args[:cloud_id]]
206
+ end
198
207
  else
199
208
  resp = MU::Cloud::AWS.sqs(region: args[:region], credentials: args[:credentials]).list_queues
200
209
  resp.queue_urls
@@ -47,7 +47,11 @@ module MU
47
47
  }
48
48
  end
49
49
 
50
+ type_map = {}
51
+
50
52
  @config['attributes'].each { |attr|
53
+ type_map[attr['name']] = attr['type']
54
+
51
55
  params[:attribute_definitions] << {
52
56
  :attribute_name => attr['name'],
53
57
  :attribute_type => attr['type']
@@ -67,6 +71,11 @@ module MU
67
71
  }
68
72
  end
69
73
  }
74
+ # apparently the HASH key always has to be before RANGE, so sort it
75
+ # lexically by that field and call it a day
76
+ params[:key_schema].sort! { |a, b|
77
+ a[:key_type] <=> b[:key_type]
78
+ }
70
79
 
71
80
  if @config['secondary_indexes']
72
81
  @config['secondary_indexes'].each { |idx|
@@ -99,7 +108,11 @@ module MU
99
108
  }
100
109
  end
101
110
 
102
- MU.log "Creating DynamoDB table #{@mu_name}", details: params
111
+ if @tags
112
+ params[:tags] = @tags.each_key.map { |k| { :key => k, :value => @tags[k] } }
113
+ end
114
+
115
+ MU.log "Creating DynamoDB table #{@mu_name}", MU::NOTICE, details: params
103
116
 
104
117
  resp = MU::Cloud::AWS.dynamo(credentials: @config['credentials'], region: @config['region']).create_table(params)
105
118
  @cloud_id = @mu_name
@@ -109,8 +122,24 @@ module MU
109
122
  sleep 5 if resp.table.table_status == "CREATING"
110
123
  end while resp.table.table_status == "CREATING"
111
124
 
112
-
113
125
  tagTable if !@config['scrub_mu_isms']
126
+
127
+ if @config['populate'] and !@config['populate'].empty?
128
+ MU.log "Preloading #{@mu_name} with #{@config['populate'].size.to_s} items"
129
+ items_to_write = @config['populate'].dup
130
+ begin
131
+ batch = items_to_write.slice!(0, (items_to_write.length >= 25 ? 25 : items_to_write.length))
132
+ begin
133
+ MU::Cloud::AWS.dynamo(credentials: @config['credentials'], region: @config['region']).batch_write_item(
134
+ request_items: {
135
+ @cloud_id => batch.map { |i| { put_request: { item: i } } }
136
+ }
137
+ )
138
+ rescue ::Aws::DynamoDB::Errors::ValidationException => e
139
+ MU.log e.message, MU::ERR, details: item
140
+ end
141
+ end while !items_to_write.empty?
142
+ end
114
143
  end
115
144
 
116
145
  # Apply tags to this DynamoDB table
@@ -143,6 +172,7 @@ module MU
143
172
  # Called automatically by {MU::Deploy#createResources}
144
173
  def groom
145
174
  tagTable if !@config['scrub_mu_isms']
175
+ MU.log "NoSQL Table #{@config['name']}: #{@cloud_id}", MU::SUMMARY
146
176
  end
147
177
 
148
178
  # Does this resource type exist as a global (cloud-wide) artifact, or
@@ -163,7 +193,7 @@ module MU
163
193
  # @param ignoremaster [Boolean]: If true, will remove resources not flagged as originating from this Mu server
164
194
  # @param region [String]: The cloud provider region
165
195
  # @return [void]
166
- def self.cleanup(noop: false, ignoremaster: false, region: MU.curRegion, credentials: nil, flags: {})
196
+ def self.cleanup(noop: false, deploy_id: MU.deploy_id, ignoremaster: false, region: MU.curRegion, credentials: nil, flags: {})
167
197
  MU.log "AWS::NoSQLDb.cleanup: need to support flags['known']", MU::DEBUG, details: flags
168
198
 
169
199
  resp = MU::Cloud::AWS.dynamo(credentials: credentials, region: region).list_tables
@@ -183,7 +213,7 @@ module MU
183
213
  deploy_match = false
184
214
  master_match = false
185
215
  tags.tags.each { |tag|
186
- if tag.key == "MU-ID" and tag.value == MU.deploy_id
216
+ if tag.key == "MU-ID" and tag.value == deploy_id
187
217
  deploy_match = true
188
218
  elsif tag.key == "MU-MASTER-IP" and tag.value == MU.mu_public_ip
189
219
  master_match = true
@@ -214,7 +244,8 @@ module MU
214
244
  # Return the metadata for this user cofiguration
215
245
  # @return [Hash]
216
246
  def notify
217
- MU.structToHash(cloud_desc)
247
+ return nil if !@cloud_id or !cloud_desc(use_cache: false)
248
+ MU.structToHash(cloud_desc, stringify_keys: true)
218
249
  end
219
250
 
220
251
  # Locate an existing DynamoDB table
@@ -244,6 +275,59 @@ module MU
244
275
  found
245
276
  end
246
277
 
278
+ # Reverse-map our cloud description into a runnable config hash.
279
+ # We assume that any values we have in +@config+ are placeholders, and
280
+ # calculate our own accordingly based on what's live in the cloud.
281
+ def toKitten(**_args)
282
+ bok = {
283
+ "cloud" => "AWS",
284
+ "credentials" => @config['credentials'],
285
+ "cloud_id" => @cloud_id,
286
+ "region" => @config['region']
287
+ }
288
+
289
+ if !cloud_desc
290
+ MU.log "toKitten failed to load a cloud_desc from #{@cloud_id}", MU::ERR, details: @config
291
+ return nil
292
+ end
293
+ bok['name'] = cloud_desc.table_name
294
+ bok['read_capacity'] = cloud_desc.provisioned_throughput.read_capacity_units
295
+ bok['write_capacity'] = cloud_desc.provisioned_throughput.write_capacity_units
296
+
297
+
298
+ cloud_desc.attribute_definitions.each { |attr|
299
+ bok['attributes'] ||= []
300
+ newattr = {
301
+ "name" => attr.attribute_name,
302
+ "type" => attr.attribute_type
303
+ }
304
+ if cloud_desc.key_schema
305
+ cloud_desc.key_schema.each { |key|
306
+ next if key.attribute_name == attr.attribute_name
307
+ if key.key_type == "RANGE"
308
+ newattr["primary_partition"] = true
309
+ elsif key.key_type == "HASH"
310
+ newattr["primary_sort"] = true
311
+ end
312
+ }
313
+ end
314
+ bok['attributes'] << newattr
315
+ }
316
+
317
+ if cloud_desc.stream_specification and cloud_desc.stream_specification.stream_enabled
318
+
319
+ bok['stream'] = cloud_desc.stream_specification.stream_view_type
320
+ # cloud_desc.latest_stream_arn
321
+ # MU::Cloud::AWS.dynamostream(credentials: @credentials, region: @config['region']).list_streams
322
+ end
323
+
324
+ bok["populate"] = MU::Cloud::AWS.dynamo(credentials: @credentials, region: @config['region']).scan(
325
+ table_name: @cloud_id
326
+ ).items
327
+
328
+ bok
329
+ end
330
+
247
331
  # Cloud-specific configuration properties.
248
332
  # @param _config [MU::Config]: The calling MU::Config object
249
333
  # @return [Array<Array,Hash>]: List of required fields, and json-schema Hash of cloud-specific configuration parameters for this resource
@@ -252,6 +336,13 @@ module MU
252
336
 
253
337
 
254
338
  schema = {
339
+ "populate" => {
340
+ "type" => "array",
341
+ "items" => {
342
+ "type" => "object",
343
+ "description" => "Key-value pairs, compatible with the +attributes+ schema, with which to populate this +table+ during its initial creation."
344
+ }
345
+ },
255
346
  "attributes" => {
256
347
  "type" => "array",
257
348
  "minItems" => 1,
@@ -27,8 +27,8 @@ module MU
27
27
 
28
28
  # Called automatically by {MU::Deploy#createResources}
29
29
  def create
30
- MU::Cloud::AWS.sns(region: @config['region'], credentials: @config['credentials']).create_topic(name: @mu_name)
31
30
  @cloud_id = @mu_name
31
+ MU::Cloud::AWS.sns(region: @config['region'], credentials: @config['credentials']).create_topic(name: @cloud_id)
32
32
  MU.log "Created SNS topic #{@mu_name}"
33
33
  end
34
34
 
@@ -36,17 +36,48 @@ module MU
36
36
  def groom
37
37
  if @config['subscriptions']
38
38
  @config['subscriptions'].each { |sub|
39
- MU::Cloud::AWS::Notifier.subscribe(
40
- arn: arn,
41
- endpoint: sub['endpoint'],
42
- region: @config['region'],
43
- credentials: @config['credentials'],
44
- protocol: sub['type']
45
- )
39
+ if sub['resource'] and !sub['endpoint']
40
+ endpoint_obj = nil
41
+ MU.retrier([], max: 5, wait: 9, loop_if: Proc.new { endpoint_obj.nil? }) {
42
+ endpoint_obj = MU::Config::Ref.get(sub['resource']).kitten(@deploy)
43
+ }
44
+ sub['endpoint'] = endpoint_obj.arn
45
+ end
46
+ subscribe(sub['endpoint'], sub['type'])
46
47
  }
47
48
  end
48
49
  end
49
50
 
51
+ # Subscribe something to this SNS topic
52
+ # @param endpoint [String]: The address, identifier, or ARN of the resource being subscribed
53
+ # @param protocol [String]: The protocol being subscribed
54
+ def subscribe(endpoint, protocol)
55
+ self.class.subscribe(arn, endpoint, protocol, region: @config['region'], credentials: @credentials)
56
+ end
57
+
58
+ # Subscribe something to an SNS topic
59
+ # @param cloud_id [String]: The short name or ARN of an existing SNS topic
60
+ # @param endpoint [String]: The address, identifier, or ARN of the resource being subscribed
61
+ # @param protocol [String]: The protocol being subscribed
62
+ # @param region [String]: The region of the target SNS topic
63
+ # @param credentials [String]:
64
+ def self.subscribe(cloud_id, endpoint, protocol, region: nil, credentials: nil)
65
+ topic = find(cloud_id: cloud_id, region: region, credentials: credentials).values.first
66
+ if !topic
67
+ raise MuError, "Failed to find SNS Topic #{cloud_id} in #{region}"
68
+ end
69
+ arn = topic["TopicArn"]
70
+
71
+ resp = MU::Cloud::AWS.sns(region: region, credentials: credentials).list_subscriptions_by_topic(topic_arn: arn).subscriptions
72
+
73
+ resp.each { |subscription|
74
+ return subscription if subscription.protocol == protocol and subscription.endpoint == endpoint
75
+ }
76
+
77
+ MU.log "Subscribing #{endpoint} (#{protocol}) to SNS topic #{arn}", MU::NOTICE
78
+ MU::Cloud::AWS.sns(region: region, credentials: credentials).subscribe(topic_arn: arn, protocol: protocol, endpoint: endpoint)
79
+ end
80
+
50
81
  # Does this resource type exist as a global (cloud-wide) artifact, or
51
82
  # is it localized to a region/zone?
52
83
  # @return [Boolean]
@@ -65,12 +96,12 @@ module MU
65
96
  # @param ignoremaster [Boolean]: If true, will remove resources not flagged as originating from this Mu server
66
97
  # @param region [String]: The cloud provider region
67
98
  # @return [void]
68
- def self.cleanup(noop: false, ignoremaster: false, region: MU.curRegion, credentials: nil, flags: {})
99
+ def self.cleanup(noop: false, deploy_id: MU.deploy_id, ignoremaster: false, region: MU.curRegion, credentials: nil, flags: {})
69
100
  MU.log "AWS::Notifier.cleanup: need to support flags['known']", MU::DEBUG, details: flags
70
101
  MU.log "Placeholder: AWS Notifier artifacts do not support tags, so ignoremaster cleanup flag has no effect", MU::DEBUG, details: ignoremaster
71
102
 
72
103
  MU::Cloud::AWS.sns(region: region, credentials: credentials).list_topics.topics.each { |topic|
73
- if topic.topic_arn.match(MU.deploy_id)
104
+ if topic.topic_arn.match(deploy_id)
74
105
  # We don't have a way to tag our SNS topics, so we will delete any topic that has the MU-ID in its ARN.
75
106
  # This may fail to find notifier groups in some cases (eg. cache_cluster) so we might want to delete from each API as well.
76
107
  MU.log "Deleting SNS topic: #{topic.topic_arn}"
@@ -91,6 +122,7 @@ module MU
91
122
  # Return the metadata for this user cofiguration
92
123
  # @return [Hash]
93
124
  def notify
125
+ return nil if !@cloud_id or !cloud_desc(use_cache: false)
94
126
  desc = MU::Cloud::AWS.sns(region: @config["region"], credentials: @config["credentials"]).get_topic_attributes(topic_arn: arn).attributes
95
127
  MU.structToHash(desc)
96
128
  end
@@ -101,9 +133,16 @@ module MU
101
133
  found = {}
102
134
 
103
135
  if args[:cloud_id]
104
- arn = "arn:"+(MU::Cloud::AWS.isGovCloud?(args[:region]) ? "aws-us-gov" : "aws")+":sns:"+args[:region]+":"+MU::Cloud::AWS.credToAcct(args[:credentials])+":"+args[:cloud_id]
105
- desc = MU::Cloud::AWS.sns(region: args[:region], credentials: args[:credentials]).get_topic_attributes(topic_arn: arn).attributes
106
- found[args[:cloud_id]] = desc if desc
136
+ arn = if args[:cloud_id].match(/^arn:/)
137
+ args[:cloud_id]
138
+ else
139
+ "arn:"+(MU::Cloud::AWS.isGovCloud?(args[:region]) ? "aws-us-gov" : "aws")+":sns:"+args[:region]+":"+MU::Cloud::AWS.credToAcct(args[:credentials])+":"+args[:cloud_id]
140
+ end
141
+ begin
142
+ desc = MU::Cloud::AWS.sns(region: args[:region], credentials: args[:credentials]).get_topic_attributes(topic_arn: arn).attributes
143
+ found[args[:cloud_id]] = desc if desc
144
+ rescue ::Aws::SNS::Errors::NotFound
145
+ end
107
146
  else
108
147
  next_token = nil
109
148
  begin
@@ -120,6 +159,58 @@ module MU
120
159
  found
121
160
  end
122
161
 
162
+ # Reverse-map our cloud description into a runnable config hash.
163
+ # We assume that any values we have in +@config+ are placeholders, and
164
+ # calculate our own accordingly based on what's live in the cloud.
165
+ def toKitten(**_args)
166
+ bok = {
167
+ "cloud" => "AWS",
168
+ "credentials" => @config['credentials'],
169
+ "cloud_id" => @cloud_id,
170
+ "region" => @config['region']
171
+ }
172
+
173
+ if !cloud_desc
174
+ MU.log "toKitten failed to load a cloud_desc from #{@cloud_id}", MU::ERR, details: @config
175
+ return nil
176
+ end
177
+
178
+ bok['name'] = cloud_desc["DisplayName"].empty? ? @cloud_id : cloud_desc["DisplayName"]
179
+ svcmap = {
180
+ "lambda" => "functions",
181
+ "sqs" => "msg_queues"
182
+ }
183
+ MU::Cloud::AWS.sns(region: @config['region'], credentials: @credentials).list_subscriptions_by_topic(topic_arn: cloud_desc["TopicArn"]).subscriptions.each { |sub|
184
+ bok['subscriptions'] ||= []
185
+
186
+ bok['subscriptions'] << if sub.endpoint.match(/^arn:[^:]+:(sqs|lambda):([^:]+):(\d+):.*?([^:\/]+)$/)
187
+ _wholestring, service, region, account, id = Regexp.last_match.to_a
188
+ {
189
+ "type" => sub.protocol,
190
+ "resource" => MU::Config::Ref.get(
191
+ type: svcmap[service],
192
+ region: region,
193
+ credentials: @credentials,
194
+ id: id,
195
+ cloud: "AWS",
196
+ habitat: MU::Config::Ref.get(
197
+ id: account,
198
+ cloud: "AWS",
199
+ credentials: @credentials
200
+ )
201
+ )
202
+ }
203
+ else
204
+ {
205
+ "type" => sub.protocol,
206
+ "endpoint" => sub.endpoint
207
+ }
208
+ end
209
+ }
210
+
211
+ bok
212
+ end
213
+
123
214
  # Cloud-specific configuration properties.
124
215
  # @param _config [MU::Config]: The calling MU::Config object
125
216
  # @return [Array<Array,Hash>]: List of required fields, and json-schema Hash of cloud-specific configuration parameters for this resource
@@ -130,11 +221,10 @@ module MU
130
221
  "type" => "array",
131
222
  "items" => {
132
223
  "type" => "object",
133
- "required" => ["endpoint"],
134
224
  "properties" => {
135
225
  "type" => {
136
226
  "type" => "string",
137
- "description" => "",
227
+ "description" => "Type of endpoint or resource which should receive notifications. If not specified, will attempt to auto-detect.",
138
228
  "enum" => ["http", "https", "email", "email-json", "sms", "sqs", "application", "lambda"]
139
229
  }
140
230
  }
@@ -148,26 +238,42 @@ module MU
148
238
  # Cloud-specific pre-processing of {MU::Config::BasketofKittens::notifier}, bare and unvalidated.
149
239
 
150
240
  # @param notifier [Hash]: The resource to process and validate
151
- # @param _configurator [MU::Config]: The overall deployment configurator of which this resource is a member
241
+ # @param configurator [MU::Config]: The overall deployment configurator of which this resource is a member
152
242
  # @return [Boolean]: True if validation succeeded, False otherwise
153
- def self.validateConfig(notifier, _configurator)
243
+ def self.validateConfig(notifier, configurator)
154
244
  ok = true
155
245
 
156
246
  if notifier['subscriptions']
157
247
  notifier['subscriptions'].each { |sub|
248
+ if sub['resource'] and configurator.haveLitterMate?(sub['resource']['name'], sub['resource']['type'])
249
+ sub['resource']['cloud'] = "AWS"
250
+ MU::Config.addDependency(notifier, sub['resource']['name'], sub['resource']['type'])
251
+ end
158
252
  if !sub["type"]
159
- if sub["endpoint"].match(/^http:/i)
160
- sub["type"] = "http"
161
- elsif sub["endpoint"].match(/^https:/i)
162
- sub["type"] = "https"
163
- elsif sub["endpoint"].match(/^sqs:/i)
164
- sub["type"] = "sqs"
165
- elsif sub["endpoint"].match(/^\+?[\d\-]+$/)
166
- sub["type"] = "sms"
167
- elsif sub["endpoint"].match(/\A[\w+\-.]+@[a-z\d\-]+(\.[a-z]+)*\.[a-z]+\z/i)
168
- sub["type"] = "email"
169
- else
170
- MU.log "Notifier #{notifier['name']} subscription #{sub['endpoint']} did not specify a type, and I'm unable to guess one", MU::ERR
253
+ sub['type'] = if sub['resource']
254
+ if sub['resource']['type'] == "functions"
255
+ "lambda"
256
+ elsif sub['resource']['type'] == "msg_queues"
257
+ "sqs"
258
+ end
259
+ elsif sub['endpoint']
260
+ if sub["endpoint"].match(/^http:/i)
261
+ "http"
262
+ elsif sub["endpoint"].match(/^https:/i)
263
+ "https"
264
+ elsif sub["endpoint"].match(/:sqs:/i)
265
+ "sqs"
266
+ elsif sub["endpoint"].match(/:lambda:/i)
267
+ "lambda"
268
+ elsif sub["endpoint"].match(/^\+?[\d\-]+$/)
269
+ "sms"
270
+ elsif sub["endpoint"].match(/\A[\w+\-.]+@[a-z\d\-]+(\.[a-z]+)*\.[a-z]+\z/i)
271
+ "email"
272
+ end
273
+ end
274
+
275
+ if !sub['type']
276
+ MU.log "Notifier #{notifier['name']} subscription did not specify a type, and I'm unable to guess one", MU::ERR, details: sub
171
277
  ok = false
172
278
  end
173
279
  end
@@ -178,40 +284,6 @@ module MU
178
284
  end
179
285
 
180
286
 
181
- # Subscribe to a notifier group. This can either be an email address, SQS queue, application endpoint, etc...
182
- # Will create the subscription only if it doesn't already exist.
183
- # @param arn [String]: The cloud provider's identifier of the notifier group.
184
- # @param protocol [String]: The type of the subscription (eg. email,https, etc..).
185
- # @param endpoint [String]: The endpoint of the subscription. This will depend on the 'protocol' (as an example if protocol is email, endpoint will be the email address) ..
186
- # @param region [String]: The cloud provider region.
187
- def self.subscribe(arn: nil, protocol: nil, endpoint: nil, region: MU.curRegion, credentials: nil)
188
- retries = 0
189
- begin
190
- resp = MU::Cloud::AWS.sns(region: region, credentials: credentials).list_subscriptions_by_topic(topic_arn: arn).subscriptions
191
- rescue Aws::SNS::Errors::NotFound
192
- if retries < 5
193
- MU.log "Couldn't find topic #{arn}, retrying several times in case of a lagging resource"
194
- retries += 1
195
- sleep 30
196
- retry
197
- else
198
- raise MuError, "Couldn't find topic #{arn}, giving up"
199
- end
200
- end
201
-
202
- already_subscribed = false
203
- if resp && !resp.empty?
204
- resp.each { |subscription|
205
- already_subscribed = true if subscription.protocol == protocol && subscription.endpoint == endpoint
206
- }
207
- end
208
-
209
- unless already_subscribed
210
- MU::Cloud::AWS.sns(region: region, credentials: credentials).subscribe(topic_arn: arn, protocol: protocol, endpoint: endpoint)
211
- MU.log "Subscribed #{endpoint} to SNS topic #{arn}"
212
- end
213
- end
214
-
215
287
  # Test if a notifier group exists
216
288
  # Create a new notifier group. Will check if the group exists before creating it.
217
289
  # @param topic_name [String]: The cloud provider's name for the notifier group.