cloud-mu 3.2.0 → 3.5.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 (156) hide show
  1. checksums.yaml +4 -4
  2. data/Dockerfile +1 -1
  3. data/ansible/roles/mu-nat/tasks/main.yml +3 -0
  4. data/bin/mu-adopt +12 -1
  5. data/bin/mu-aws-setup +41 -7
  6. data/bin/mu-azure-setup +34 -0
  7. data/bin/mu-configure +214 -119
  8. data/bin/mu-gcp-setup +37 -2
  9. data/bin/mu-load-config.rb +2 -1
  10. data/bin/mu-node-manage +3 -0
  11. data/bin/mu-refresh-ssl +67 -0
  12. data/bin/mu-run-tests +28 -6
  13. data/bin/mu-self-update +30 -10
  14. data/bin/mu-upload-chef-artifacts +30 -26
  15. data/cloud-mu.gemspec +10 -8
  16. data/cookbooks/mu-master/attributes/default.rb +5 -1
  17. data/cookbooks/mu-master/metadata.rb +2 -2
  18. data/cookbooks/mu-master/recipes/default.rb +81 -26
  19. data/cookbooks/mu-master/recipes/init.rb +197 -62
  20. data/cookbooks/mu-master/recipes/update_nagios_only.rb +1 -1
  21. data/cookbooks/mu-master/recipes/vault.rb +78 -77
  22. data/cookbooks/mu-master/templates/default/mods/rewrite.conf.erb +1 -0
  23. data/cookbooks/mu-master/templates/default/nagios.conf.erb +103 -0
  24. data/cookbooks/mu-master/templates/default/web_app.conf.erb +14 -30
  25. data/cookbooks/mu-tools/attributes/default.rb +12 -0
  26. data/cookbooks/mu-tools/files/centos-6/CentOS-Base.repo +47 -0
  27. data/cookbooks/mu-tools/libraries/helper.rb +98 -4
  28. data/cookbooks/mu-tools/libraries/monkey.rb +1 -1
  29. data/cookbooks/mu-tools/recipes/apply_security.rb +31 -9
  30. data/cookbooks/mu-tools/recipes/aws_api.rb +8 -2
  31. data/cookbooks/mu-tools/recipes/base_repositories.rb +1 -1
  32. data/cookbooks/mu-tools/recipes/gcloud.rb +2 -9
  33. data/cookbooks/mu-tools/recipes/google_api.rb +7 -0
  34. data/cookbooks/mu-tools/recipes/rsyslog.rb +8 -1
  35. data/cookbooks/mu-tools/resources/disk.rb +113 -42
  36. data/cookbooks/mu-tools/resources/mommacat_request.rb +1 -2
  37. data/cookbooks/mu-tools/templates/centos-8/sshd_config.erb +215 -0
  38. data/extras/Gemfile.lock.bootstrap +394 -0
  39. data/extras/bucketstubs/error.html +0 -0
  40. data/extras/bucketstubs/index.html +0 -0
  41. data/extras/clean-stock-amis +11 -3
  42. data/extras/generate-stock-images +6 -3
  43. data/extras/git_rpm/build.sh +20 -0
  44. data/extras/git_rpm/mugit.spec +53 -0
  45. data/extras/image-generators/AWS/centos7.yaml +19 -16
  46. data/extras/image-generators/AWS/{rhel7.yaml → rhel71.yaml} +0 -0
  47. data/extras/image-generators/AWS/{win2k12.yaml → win2k12r2.yaml} +0 -0
  48. data/extras/image-generators/VMWare/centos8.yaml +15 -0
  49. data/extras/openssl_rpm/build.sh +19 -0
  50. data/extras/openssl_rpm/mussl.spec +46 -0
  51. data/extras/python_rpm/muthon.spec +14 -4
  52. data/extras/ruby_rpm/muby.spec +9 -5
  53. data/extras/sqlite_rpm/build.sh +19 -0
  54. data/extras/sqlite_rpm/muqlite.spec +47 -0
  55. data/install/installer +7 -5
  56. data/modules/mommacat.ru +2 -2
  57. data/modules/mu.rb +14 -7
  58. data/modules/mu/adoption.rb +5 -5
  59. data/modules/mu/cleanup.rb +47 -25
  60. data/modules/mu/cloud.rb +29 -1
  61. data/modules/mu/cloud/dnszone.rb +0 -2
  62. data/modules/mu/cloud/machine_images.rb +1 -1
  63. data/modules/mu/cloud/providers.rb +6 -1
  64. data/modules/mu/cloud/resource_base.rb +16 -7
  65. data/modules/mu/cloud/ssh_sessions.rb +5 -1
  66. data/modules/mu/cloud/wrappers.rb +20 -7
  67. data/modules/mu/config.rb +28 -12
  68. data/modules/mu/config/bucket.rb +31 -2
  69. data/modules/mu/config/cache_cluster.rb +1 -1
  70. data/modules/mu/config/cdn.rb +100 -0
  71. data/modules/mu/config/container_cluster.rb +1 -1
  72. data/modules/mu/config/database.rb +3 -3
  73. data/modules/mu/config/dnszone.rb +4 -3
  74. data/modules/mu/config/endpoint.rb +1 -0
  75. data/modules/mu/config/firewall_rule.rb +1 -1
  76. data/modules/mu/config/function.rb +16 -7
  77. data/modules/mu/config/job.rb +89 -0
  78. data/modules/mu/config/notifier.rb +7 -18
  79. data/modules/mu/config/ref.rb +55 -9
  80. data/modules/mu/config/schema_helpers.rb +12 -3
  81. data/modules/mu/config/server.rb +11 -5
  82. data/modules/mu/config/server_pool.rb +2 -2
  83. data/modules/mu/config/vpc.rb +11 -10
  84. data/modules/mu/defaults/AWS.yaml +106 -106
  85. data/modules/mu/deploy.rb +40 -14
  86. data/modules/mu/groomers/chef.rb +2 -2
  87. data/modules/mu/master.rb +70 -3
  88. data/modules/mu/mommacat.rb +28 -9
  89. data/modules/mu/mommacat/daemon.rb +13 -7
  90. data/modules/mu/mommacat/naming.rb +2 -2
  91. data/modules/mu/mommacat/search.rb +16 -5
  92. data/modules/mu/mommacat/storage.rb +67 -32
  93. data/modules/mu/providers/aws.rb +298 -85
  94. data/modules/mu/providers/aws/alarm.rb +5 -5
  95. data/modules/mu/providers/aws/bucket.rb +284 -50
  96. data/modules/mu/providers/aws/cache_cluster.rb +26 -26
  97. data/modules/mu/providers/aws/cdn.rb +782 -0
  98. data/modules/mu/providers/aws/collection.rb +16 -16
  99. data/modules/mu/providers/aws/container_cluster.rb +84 -64
  100. data/modules/mu/providers/aws/database.rb +59 -55
  101. data/modules/mu/providers/aws/dnszone.rb +29 -12
  102. data/modules/mu/providers/aws/endpoint.rb +535 -50
  103. data/modules/mu/providers/aws/firewall_rule.rb +32 -26
  104. data/modules/mu/providers/aws/folder.rb +1 -1
  105. data/modules/mu/providers/aws/function.rb +300 -134
  106. data/modules/mu/providers/aws/group.rb +16 -14
  107. data/modules/mu/providers/aws/habitat.rb +4 -4
  108. data/modules/mu/providers/aws/job.rb +469 -0
  109. data/modules/mu/providers/aws/loadbalancer.rb +67 -45
  110. data/modules/mu/providers/aws/log.rb +17 -17
  111. data/modules/mu/providers/aws/msg_queue.rb +22 -13
  112. data/modules/mu/providers/aws/nosqldb.rb +99 -8
  113. data/modules/mu/providers/aws/notifier.rb +137 -65
  114. data/modules/mu/providers/aws/role.rb +119 -83
  115. data/modules/mu/providers/aws/search_domain.rb +166 -30
  116. data/modules/mu/providers/aws/server.rb +209 -118
  117. data/modules/mu/providers/aws/server_pool.rb +95 -130
  118. data/modules/mu/providers/aws/storage_pool.rb +19 -11
  119. data/modules/mu/providers/aws/user.rb +5 -5
  120. data/modules/mu/providers/aws/userdata/linux.erb +5 -4
  121. data/modules/mu/providers/aws/vpc.rb +109 -54
  122. data/modules/mu/providers/aws/vpc_subnet.rb +43 -39
  123. data/modules/mu/providers/azure.rb +78 -12
  124. data/modules/mu/providers/azure/server.rb +20 -4
  125. data/modules/mu/providers/cloudformation/server.rb +1 -1
  126. data/modules/mu/providers/google.rb +21 -5
  127. data/modules/mu/providers/google/bucket.rb +1 -1
  128. data/modules/mu/providers/google/container_cluster.rb +1 -1
  129. data/modules/mu/providers/google/database.rb +1 -1
  130. data/modules/mu/providers/google/firewall_rule.rb +1 -1
  131. data/modules/mu/providers/google/folder.rb +7 -3
  132. data/modules/mu/providers/google/function.rb +66 -31
  133. data/modules/mu/providers/google/group.rb +1 -1
  134. data/modules/mu/providers/google/habitat.rb +1 -1
  135. data/modules/mu/providers/google/loadbalancer.rb +1 -1
  136. data/modules/mu/providers/google/role.rb +6 -3
  137. data/modules/mu/providers/google/server.rb +1 -1
  138. data/modules/mu/providers/google/server_pool.rb +1 -1
  139. data/modules/mu/providers/google/user.rb +1 -1
  140. data/modules/mu/providers/google/vpc.rb +28 -3
  141. data/modules/tests/aws-jobs-functions.yaml +46 -0
  142. data/modules/tests/aws-servers-with-handrolled-iam.yaml +37 -0
  143. data/modules/tests/centos6.yaml +4 -0
  144. data/modules/tests/centos7.yaml +4 -0
  145. data/modules/tests/ecs.yaml +2 -2
  146. data/modules/tests/eks.yaml +1 -1
  147. data/modules/tests/functions/node-function/lambda_function.js +10 -0
  148. data/modules/tests/functions/python-function/lambda_function.py +12 -0
  149. data/modules/tests/k8s.yaml +1 -1
  150. data/modules/tests/microservice_app.yaml +288 -0
  151. data/modules/tests/rds.yaml +5 -5
  152. data/modules/tests/regrooms/rds.yaml +5 -5
  153. data/modules/tests/server-with-scrub-muisms.yaml +1 -1
  154. data/modules/tests/super_complex_bok.yml +2 -2
  155. data/modules/tests/super_simple_bok.yml +2 -2
  156. metadata +42 -17
@@ -33,7 +33,7 @@ module MU
33
33
  namestr += ".fifo" if attrs['FifoQueue']
34
34
 
35
35
  MU.log "Creating SQS queue #{namestr}", details: attrs
36
- resp = MU::Cloud::AWS.sqs(region: @config['region'], credentials: @config['credentials']).create_queue(
36
+ resp = MU::Cloud::AWS.sqs(region: @region, credentials: @credentials).create_queue(
37
37
  queue_name: namestr,
38
38
  attributes: attrs
39
39
  )
@@ -60,7 +60,7 @@ module MU
60
60
  }
61
61
  if changed
62
62
  MU.log "Updating SQS queue #{@mu_name}", MU::NOTICE, details: new_attrs
63
- MU::Cloud::AWS.sqs(region: @config['region'], credentials: @config['credentials']).set_queue_attributes(
63
+ MU::Cloud::AWS.sqs(region: @region, credentials: @credentials).set_queue_attributes(
64
64
  queue_url: @cloud_id,
65
65
  attributes: new_attrs
66
66
  )
@@ -71,7 +71,7 @@ module MU
71
71
  # Canonical Amazon Resource Number for this resource
72
72
  # @return [String]
73
73
  def arn
74
- "arn:"+(MU::Cloud::AWS.isGovCloud?(@config["region"]) ? "aws-us-gov" : "aws")+":sqs:"+@config['region']+":"+MU::Cloud::AWS.credToAcct(@config['credentials'])+":"+@cloud_id
74
+ "arn:"+(MU::Cloud::AWS.isGovCloud?(@region) ? "aws-us-gov" : "aws")+":sqs:"+@region+":"+MU::Cloud::AWS.credToAcct(@credentials)+":"+@cloud_id
75
75
  end
76
76
 
77
77
  @cloud_desc_cache = nil
@@ -80,9 +80,10 @@ 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
- resp = MU::Cloud::AWS.sqs(region: @config['region'], credentials: @config['credentials']).list_queues(
86
+ resp = MU::Cloud::AWS.sqs(region: @region, credentials: @credentials).list_queues(
86
87
  queue_name_prefix: @mu_name
87
88
  )
88
89
  return nil if !resp or !resp.queue_urls
@@ -97,8 +98,8 @@ module MU
97
98
  return nil if !@cloud_id
98
99
  @cloud_desc_cache = MU::Cloud::AWS::MsgQueue.find(
99
100
  cloud_id: @cloud_id.dup,
100
- region: @config['region'],
101
- credentials: @config['credentials']
101
+ region: @region,
102
+ credentials: @credentials
102
103
  )
103
104
  @cloud_desc_cache
104
105
  end
@@ -109,8 +110,8 @@ module MU
109
110
  cloud_desc
110
111
  deploy_struct = MU::Cloud::AWS::MsgQueue.find(
111
112
  cloud_id: @cloud_id,
112
- region: @config['region'],
113
- credentials: @config['credentials']
113
+ region: @region,
114
+ credentials: @credentials
114
115
  )
115
116
  return deploy_struct
116
117
  end
@@ -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
@@ -417,7 +426,7 @@ module MU
417
426
  if sibling # resolve sibling queues to something useful
418
427
  id = sibling.cloud_id
419
428
  end
420
- desc = MU::Cloud::AWS::MsgQueue.find(cloud_id: id, credentials: @config['credentials'])
429
+ desc = MU::Cloud::AWS::MsgQueue.find(cloud_id: id, credentials: @credentials)
421
430
  if !desc
422
431
  raise MuError, "Failed to get cloud descriptor for SQS queue #{@config['failqueue']['name']}"
423
432
  end
@@ -475,7 +484,7 @@ module MU
475
484
  end
476
485
 
477
486
  begin
478
- MU::Cloud::AWS.sqs(region: @config['region'], credentials: @config['credentials']).tag_queue(
487
+ MU::Cloud::AWS.sqs(region: @region, credentials: @credentials).tag_queue(
479
488
  queue_url: url,
480
489
  tags: tags
481
490
  )
@@ -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,18 +108,38 @@ 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
- resp = MU::Cloud::AWS.dynamo(credentials: @config['credentials'], region: @config['region']).create_table(params)
117
+ resp = MU::Cloud::AWS.dynamo(credentials: @credentials, region: @region).create_table(params)
105
118
  @cloud_id = @mu_name
106
119
 
107
120
  begin
108
- resp = MU::Cloud::AWS.dynamo(credentials: @config['credentials'], region: @config['region']).describe_table(table_name: @cloud_id)
121
+ resp = MU::Cloud::AWS.dynamo(credentials: @credentials, region: @region).describe_table(table_name: @cloud_id)
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: @credentials, region: @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
@@ -133,7 +162,7 @@ module MU
133
162
  }
134
163
  end
135
164
 
136
- MU::Cloud::AWS.dynamo(credentials: @config['credentials'], region: @config['region']).tag_resource(
165
+ MU::Cloud::AWS.dynamo(credentials: @credentials, region: @region).tag_resource(
137
166
  resource_arn: arn,
138
167
  tags: tagset
139
168
  )
@@ -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" => @credentials,
285
+ "cloud_id" => @cloud_id,
286
+ "region" => @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: @region).list_streams
322
+ end
323
+
324
+ bok["populate"] = MU::Cloud::AWS.dynamo(credentials: @credentials, region: @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: @region, credentials: @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: @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}"
@@ -85,13 +116,14 @@ module MU
85
116
  # @return [String]
86
117
  def arn
87
118
  @cloud_id ||= @mu_name
88
- "arn:"+(MU::Cloud::AWS.isGovCloud?(@config["region"]) ? "aws-us-gov" : "aws")+":sns:"+@config['region']+":"+MU::Cloud::AWS.credToAcct(@config['credentials'])+":"+@cloud_id
119
+ "arn:"+(MU::Cloud::AWS.isGovCloud?(@region) ? "aws-us-gov" : "aws")+":sns:"+@region+":"+MU::Cloud::AWS.credToAcct(@credentials)+":"+@cloud_id
89
120
  end
90
121
 
91
122
  # Return the metadata for this user cofiguration
92
123
  # @return [Hash]
93
124
  def notify
94
- desc = MU::Cloud::AWS.sns(region: @config["region"], credentials: @config["credentials"]).get_topic_attributes(topic_arn: arn).attributes
125
+ return nil if !@cloud_id or !cloud_desc(use_cache: false)
126
+ desc = MU::Cloud::AWS.sns(region: @region, credentials: @credentials).get_topic_attributes(topic_arn: arn).attributes
95
127
  MU.structToHash(desc)
96
128
  end
97
129
 
@@ -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" => @credentials,
169
+ "cloud_id" => @cloud_id,
170
+ "region" => @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: @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.