cloud-mu 3.1.3 → 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 (212) hide show
  1. checksums.yaml +4 -4
  2. data/Dockerfile +15 -3
  3. data/ansible/roles/mu-windows/README.md +33 -0
  4. data/ansible/roles/mu-windows/defaults/main.yml +2 -0
  5. data/ansible/roles/mu-windows/files/LaunchConfig.json +9 -0
  6. data/ansible/roles/mu-windows/files/config.xml +76 -0
  7. data/ansible/roles/mu-windows/handlers/main.yml +2 -0
  8. data/ansible/roles/mu-windows/meta/main.yml +53 -0
  9. data/ansible/roles/mu-windows/tasks/main.yml +36 -0
  10. data/ansible/roles/mu-windows/tests/inventory +2 -0
  11. data/ansible/roles/mu-windows/tests/test.yml +5 -0
  12. data/ansible/roles/mu-windows/vars/main.yml +2 -0
  13. data/bin/mu-adopt +21 -13
  14. data/bin/mu-azure-tests +57 -0
  15. data/bin/mu-cleanup +2 -4
  16. data/bin/mu-configure +52 -0
  17. data/bin/mu-deploy +3 -3
  18. data/bin/mu-findstray-tests +25 -0
  19. data/bin/mu-gen-docs +2 -4
  20. data/bin/mu-load-config.rb +4 -4
  21. data/bin/mu-node-manage +15 -16
  22. data/bin/mu-run-tests +147 -37
  23. data/cloud-mu.gemspec +22 -20
  24. data/cookbooks/mu-activedirectory/resources/domain.rb +4 -4
  25. data/cookbooks/mu-activedirectory/resources/domain_controller.rb +4 -4
  26. data/cookbooks/mu-tools/libraries/helper.rb +3 -2
  27. data/cookbooks/mu-tools/libraries/monkey.rb +35 -0
  28. data/cookbooks/mu-tools/recipes/apply_security.rb +14 -14
  29. data/cookbooks/mu-tools/recipes/aws_api.rb +9 -0
  30. data/cookbooks/mu-tools/recipes/eks.rb +2 -2
  31. data/cookbooks/mu-tools/recipes/google_api.rb +2 -2
  32. data/cookbooks/mu-tools/recipes/selinux.rb +2 -1
  33. data/cookbooks/mu-tools/recipes/windows-client.rb +163 -164
  34. data/cookbooks/mu-tools/resources/disk.rb +1 -1
  35. data/cookbooks/mu-tools/resources/windows_users.rb +44 -43
  36. data/extras/clean-stock-amis +25 -19
  37. data/extras/generate-stock-images +1 -0
  38. data/extras/image-generators/AWS/win2k12.yaml +18 -13
  39. data/extras/image-generators/AWS/win2k16.yaml +18 -13
  40. data/extras/image-generators/AWS/win2k19.yaml +21 -0
  41. data/extras/image-generators/Google/centos6.yaml +1 -0
  42. data/extras/image-generators/Google/centos7.yaml +1 -1
  43. data/modules/mommacat.ru +6 -16
  44. data/modules/mu.rb +158 -111
  45. data/modules/mu/adoption.rb +404 -71
  46. data/modules/mu/cleanup.rb +221 -306
  47. data/modules/mu/cloud.rb +129 -1633
  48. data/modules/mu/cloud/database.rb +49 -0
  49. data/modules/mu/cloud/dnszone.rb +44 -0
  50. data/modules/mu/cloud/machine_images.rb +212 -0
  51. data/modules/mu/cloud/providers.rb +81 -0
  52. data/modules/mu/cloud/resource_base.rb +926 -0
  53. data/modules/mu/cloud/server.rb +40 -0
  54. data/modules/mu/cloud/server_pool.rb +1 -0
  55. data/modules/mu/cloud/ssh_sessions.rb +228 -0
  56. data/modules/mu/cloud/winrm_sessions.rb +237 -0
  57. data/modules/mu/cloud/wrappers.rb +169 -0
  58. data/modules/mu/config.rb +171 -1767
  59. data/modules/mu/config/alarm.rb +2 -6
  60. data/modules/mu/config/bucket.rb +32 -3
  61. data/modules/mu/config/cache_cluster.rb +2 -2
  62. data/modules/mu/config/cdn.rb +100 -0
  63. data/modules/mu/config/collection.rb +4 -4
  64. data/modules/mu/config/container_cluster.rb +9 -4
  65. data/modules/mu/config/database.rb +84 -105
  66. data/modules/mu/config/database.yml +1 -2
  67. data/modules/mu/config/dnszone.rb +10 -9
  68. data/modules/mu/config/doc_helpers.rb +516 -0
  69. data/modules/mu/config/endpoint.rb +5 -4
  70. data/modules/mu/config/firewall_rule.rb +103 -4
  71. data/modules/mu/config/folder.rb +4 -4
  72. data/modules/mu/config/function.rb +19 -10
  73. data/modules/mu/config/group.rb +4 -4
  74. data/modules/mu/config/habitat.rb +4 -4
  75. data/modules/mu/config/job.rb +89 -0
  76. data/modules/mu/config/loadbalancer.rb +60 -14
  77. data/modules/mu/config/log.rb +4 -4
  78. data/modules/mu/config/msg_queue.rb +4 -4
  79. data/modules/mu/config/nosqldb.rb +4 -4
  80. data/modules/mu/config/notifier.rb +10 -21
  81. data/modules/mu/config/ref.rb +411 -0
  82. data/modules/mu/config/role.rb +4 -4
  83. data/modules/mu/config/schema_helpers.rb +509 -0
  84. data/modules/mu/config/search_domain.rb +4 -4
  85. data/modules/mu/config/server.rb +98 -71
  86. data/modules/mu/config/server.yml +1 -0
  87. data/modules/mu/config/server_pool.rb +5 -9
  88. data/modules/mu/config/storage_pool.rb +1 -1
  89. data/modules/mu/config/tail.rb +200 -0
  90. data/modules/mu/config/user.rb +4 -4
  91. data/modules/mu/config/vpc.rb +71 -27
  92. data/modules/mu/config/vpc.yml +0 -1
  93. data/modules/mu/defaults/AWS.yaml +91 -68
  94. data/modules/mu/defaults/Azure.yaml +1 -0
  95. data/modules/mu/defaults/Google.yaml +3 -2
  96. data/modules/mu/deploy.rb +43 -26
  97. data/modules/mu/groomer.rb +17 -2
  98. data/modules/mu/groomers/ansible.rb +188 -41
  99. data/modules/mu/groomers/chef.rb +116 -55
  100. data/modules/mu/logger.rb +127 -148
  101. data/modules/mu/master.rb +410 -2
  102. data/modules/mu/master/chef.rb +3 -4
  103. data/modules/mu/master/ldap.rb +3 -3
  104. data/modules/mu/master/ssl.rb +12 -3
  105. data/modules/mu/mommacat.rb +218 -2612
  106. data/modules/mu/mommacat/daemon.rb +403 -0
  107. data/modules/mu/mommacat/naming.rb +473 -0
  108. data/modules/mu/mommacat/search.rb +495 -0
  109. data/modules/mu/mommacat/storage.rb +722 -0
  110. data/modules/mu/{clouds → providers}/README.md +1 -1
  111. data/modules/mu/{clouds → providers}/aws.rb +380 -122
  112. data/modules/mu/{clouds → providers}/aws/alarm.rb +7 -5
  113. data/modules/mu/{clouds → providers}/aws/bucket.rb +297 -59
  114. data/modules/mu/{clouds → providers}/aws/cache_cluster.rb +37 -71
  115. data/modules/mu/providers/aws/cdn.rb +782 -0
  116. data/modules/mu/{clouds → providers}/aws/collection.rb +26 -25
  117. data/modules/mu/{clouds → providers}/aws/container_cluster.rb +724 -744
  118. data/modules/mu/providers/aws/database.rb +1744 -0
  119. data/modules/mu/{clouds → providers}/aws/dnszone.rb +88 -70
  120. data/modules/mu/providers/aws/endpoint.rb +1072 -0
  121. data/modules/mu/{clouds → providers}/aws/firewall_rule.rb +220 -247
  122. data/modules/mu/{clouds → providers}/aws/folder.rb +8 -8
  123. data/modules/mu/{clouds → providers}/aws/function.rb +300 -142
  124. data/modules/mu/{clouds → providers}/aws/group.rb +31 -29
  125. data/modules/mu/{clouds → providers}/aws/habitat.rb +18 -15
  126. data/modules/mu/providers/aws/job.rb +466 -0
  127. data/modules/mu/{clouds → providers}/aws/loadbalancer.rb +66 -56
  128. data/modules/mu/{clouds → providers}/aws/log.rb +17 -14
  129. data/modules/mu/{clouds → providers}/aws/msg_queue.rb +29 -19
  130. data/modules/mu/{clouds → providers}/aws/nosqldb.rb +114 -16
  131. data/modules/mu/{clouds → providers}/aws/notifier.rb +142 -65
  132. data/modules/mu/{clouds → providers}/aws/role.rb +158 -118
  133. data/modules/mu/{clouds → providers}/aws/search_domain.rb +201 -59
  134. data/modules/mu/{clouds → providers}/aws/server.rb +844 -1139
  135. data/modules/mu/{clouds → providers}/aws/server_pool.rb +74 -65
  136. data/modules/mu/{clouds → providers}/aws/storage_pool.rb +26 -44
  137. data/modules/mu/{clouds → providers}/aws/user.rb +24 -25
  138. data/modules/mu/{clouds → providers}/aws/userdata/README.md +0 -0
  139. data/modules/mu/{clouds → providers}/aws/userdata/linux.erb +5 -4
  140. data/modules/mu/{clouds → providers}/aws/userdata/windows.erb +2 -1
  141. data/modules/mu/{clouds → providers}/aws/vpc.rb +525 -931
  142. data/modules/mu/providers/aws/vpc_subnet.rb +286 -0
  143. data/modules/mu/{clouds → providers}/azure.rb +29 -9
  144. data/modules/mu/{clouds → providers}/azure/container_cluster.rb +3 -8
  145. data/modules/mu/{clouds → providers}/azure/firewall_rule.rb +18 -11
  146. data/modules/mu/{clouds → providers}/azure/habitat.rb +8 -6
  147. data/modules/mu/{clouds → providers}/azure/loadbalancer.rb +5 -5
  148. data/modules/mu/{clouds → providers}/azure/role.rb +8 -10
  149. data/modules/mu/{clouds → providers}/azure/server.rb +97 -49
  150. data/modules/mu/{clouds → providers}/azure/user.rb +6 -8
  151. data/modules/mu/{clouds → providers}/azure/userdata/README.md +0 -0
  152. data/modules/mu/{clouds → providers}/azure/userdata/linux.erb +0 -0
  153. data/modules/mu/{clouds → providers}/azure/userdata/windows.erb +0 -0
  154. data/modules/mu/{clouds → providers}/azure/vpc.rb +16 -21
  155. data/modules/mu/{clouds → providers}/cloudformation.rb +18 -7
  156. data/modules/mu/{clouds → providers}/cloudformation/alarm.rb +3 -3
  157. data/modules/mu/{clouds → providers}/cloudformation/cache_cluster.rb +3 -3
  158. data/modules/mu/{clouds → providers}/cloudformation/collection.rb +3 -3
  159. data/modules/mu/{clouds → providers}/cloudformation/database.rb +6 -17
  160. data/modules/mu/{clouds → providers}/cloudformation/dnszone.rb +3 -3
  161. data/modules/mu/{clouds → providers}/cloudformation/firewall_rule.rb +3 -3
  162. data/modules/mu/{clouds → providers}/cloudformation/loadbalancer.rb +3 -3
  163. data/modules/mu/{clouds → providers}/cloudformation/log.rb +3 -3
  164. data/modules/mu/{clouds → providers}/cloudformation/server.rb +7 -7
  165. data/modules/mu/{clouds → providers}/cloudformation/server_pool.rb +5 -5
  166. data/modules/mu/{clouds → providers}/cloudformation/vpc.rb +5 -7
  167. data/modules/mu/{clouds → providers}/docker.rb +0 -0
  168. data/modules/mu/{clouds → providers}/google.rb +68 -30
  169. data/modules/mu/{clouds → providers}/google/bucket.rb +13 -15
  170. data/modules/mu/{clouds → providers}/google/container_cluster.rb +85 -78
  171. data/modules/mu/{clouds → providers}/google/database.rb +11 -21
  172. data/modules/mu/{clouds → providers}/google/firewall_rule.rb +15 -14
  173. data/modules/mu/{clouds → providers}/google/folder.rb +20 -17
  174. data/modules/mu/{clouds → providers}/google/function.rb +140 -168
  175. data/modules/mu/{clouds → providers}/google/group.rb +29 -34
  176. data/modules/mu/{clouds → providers}/google/habitat.rb +21 -22
  177. data/modules/mu/{clouds → providers}/google/loadbalancer.rb +19 -21
  178. data/modules/mu/{clouds → providers}/google/role.rb +94 -58
  179. data/modules/mu/{clouds → providers}/google/server.rb +243 -156
  180. data/modules/mu/{clouds → providers}/google/server_pool.rb +26 -45
  181. data/modules/mu/{clouds → providers}/google/user.rb +95 -31
  182. data/modules/mu/{clouds → providers}/google/userdata/README.md +0 -0
  183. data/modules/mu/{clouds → providers}/google/userdata/linux.erb +0 -0
  184. data/modules/mu/{clouds → providers}/google/userdata/windows.erb +0 -0
  185. data/modules/mu/{clouds → providers}/google/vpc.rb +103 -79
  186. data/modules/tests/aws-jobs-functions.yaml +46 -0
  187. data/modules/tests/bucket.yml +4 -0
  188. data/modules/tests/centos6.yaml +15 -0
  189. data/modules/tests/centos7.yaml +15 -0
  190. data/modules/tests/centos8.yaml +12 -0
  191. data/modules/tests/ecs.yaml +23 -0
  192. data/modules/tests/eks.yaml +1 -1
  193. data/modules/tests/functions/node-function/lambda_function.js +10 -0
  194. data/modules/tests/functions/python-function/lambda_function.py +12 -0
  195. data/modules/tests/includes-and-params.yaml +2 -1
  196. data/modules/tests/microservice_app.yaml +288 -0
  197. data/modules/tests/rds.yaml +108 -0
  198. data/modules/tests/regrooms/aws-iam.yaml +201 -0
  199. data/modules/tests/regrooms/bucket.yml +19 -0
  200. data/modules/tests/regrooms/rds.yaml +123 -0
  201. data/modules/tests/server-with-scrub-muisms.yaml +2 -1
  202. data/modules/tests/super_complex_bok.yml +2 -2
  203. data/modules/tests/super_simple_bok.yml +3 -5
  204. data/modules/tests/win2k12.yaml +17 -5
  205. data/modules/tests/win2k16.yaml +25 -0
  206. data/modules/tests/win2k19.yaml +25 -0
  207. data/requirements.txt +1 -0
  208. data/spec/mu/clouds/azure_spec.rb +2 -2
  209. metadata +240 -154
  210. data/extras/image-generators/AWS/windows.yaml +0 -18
  211. data/modules/mu/clouds/aws/database.rb +0 -1985
  212. data/modules/mu/clouds/aws/endpoint.rb +0 -592
@@ -42,7 +42,7 @@ module MU
42
42
  params = {
43
43
  :name => @config['name'],
44
44
  :hosted_zone_config => {
45
- :comment => MU.deploy_id
45
+ :comment => @deploy.deploy_id
46
46
  },
47
47
  :caller_reference => @deploy.getResourceName(@config['name'])
48
48
  }
@@ -173,11 +173,29 @@ module MU
173
173
  return resp.hosted_zone if @config["create_zone"]
174
174
  end
175
175
 
176
+ # Resolve a record entry (as in {MU::Config::BasketofKittens::dnszones::records} to the full DNS name we would assign it
177
+ def self.recordToName(record)
178
+ shortname = record['name']
179
+ shortname += ".#{MU.environment.downcase}" if record["append_environment_name"]
180
+
181
+ zone = if record['zone'].has_key?("id")
182
+ MU::Cloud::DNSZone.find(cloud_id: record['zone']['id']).values.first
183
+ else
184
+ MU::Cloud::DNSZone.find(cloud_id: record['zone']['name']).values.first
185
+ end
186
+
187
+ if zone.nil?
188
+ raise MuError.new "Failed to locate Route53 DNS Zone", details: record['zone']
189
+ end
190
+
191
+ shortname+"."+zone.name.sub(/\.$/, '')
192
+ end
193
+
176
194
  # Wrapper for {MU::Cloud::AWS::DNSZone.manageRecord}. Spawns threads to create all
177
195
  # requested records in background and returns immediately.
178
196
  # @param cfg [Array]: An array of parsed {MU::Config::BasketofKittens::dnszones::records} objects.
179
197
  # @param target [String]: Optional target for the records to be created. Overrides targets embedded in cfg records.
180
- def self.createRecordsFromConfig(cfg, target: nil)
198
+ def self.createRecordsFromConfig(cfg, target: nil, name_only: false)
181
199
  return if cfg.nil?
182
200
  record_threads = []
183
201
 
@@ -190,7 +208,6 @@ module MU
190
208
  zone = MU::Cloud::DNSZone.find(cloud_id: record['zone']['name']).values.first
191
209
  end
192
210
 
193
- raise MuError, "Failed to locate Route53 DNS Zone for domain #{record['zone']['name']}" if zone.nil?
194
211
 
195
212
  healthcheck_id = nil
196
213
  record['target'] = target if !target.nil?
@@ -344,8 +361,8 @@ module MU
344
361
  )
345
362
  rescue Aws::Route53::Errors::LastVPCAssociation => e
346
363
  MU.log e.inspect, MU::WARN
347
- rescue Aws::Route53::Errors::VPCAssociationNotFound => e
348
- MU.log "VPC #{vpc_id} access to zone #{id} already revoked", MU::WARN
364
+ rescue Aws::Route53::Errors::VPCAssociationNotFound
365
+ MU.log "VPC #{vpc_id} access to zone #{id} already revoked", MU::NOTICE
349
366
  end
350
367
  end
351
368
  end
@@ -366,10 +383,10 @@ module MU
366
383
  # @param location [Hash<String>]: A parsed Hash of {MU::Config::BasketofKittens::dnszones::records::geo_location}.
367
384
  # @param set_identifier [String]: A unique string to differentiate otherwise-similar records. Normally auto-generated, should not need to specify.
368
385
  # @param alias_zone [String]: Zone ID of the target's hosted zone, when creating an alias (type R53ALIAS)
369
- def self.manageRecord(id, name, type, targets: nil, aliases: nil,
386
+ def self.manageRecord(id, name, type, targets: nil,
370
387
  ttl: 7200, delete: false, sync_wait: true, failover: nil,
371
388
  healthcheck: nil, region: nil, weight: nil, overwrite: true,
372
- location: nil, set_identifier: nil, alias_zone: nil)
389
+ location: nil, set_identifier: nil, alias_zone: nil, noop: false)
373
390
 
374
391
  MU.setVar("curRegion", region) if !region.nil?
375
392
  zone = MU::Cloud::DNSZone.find(cloud_id: id).values.first
@@ -380,6 +397,11 @@ module MU
380
397
  action = "UPSERT" if overwrite
381
398
  action = "DELETE" if delete
382
399
 
400
+ record_sets = MU::Cloud::AWS.route53.list_resource_record_sets(
401
+ hosted_zone_id: id,
402
+ start_record_name: name
403
+ ).resource_record_sets if delete
404
+
383
405
  if type == "R53ALIAS"
384
406
  target_zone = id
385
407
  target_name = targets[0].downcase
@@ -413,7 +435,15 @@ module MU
413
435
  }
414
436
  else
415
437
  rrsets = []
416
- if !targets.nil?
438
+ if delete
439
+ record_sets.each { |r|
440
+ if r.name == name and r.type == type
441
+ rrsets = MU.structToHash(r.resource_records)
442
+ end
443
+ }
444
+ end
445
+
446
+ if !targets.nil? and (!delete or rrsets.empty?)
417
447
  targets.each { |target|
418
448
  rrsets << {value: target}
419
449
  }
@@ -426,6 +456,7 @@ module MU
426
456
  resource_records: rrsets
427
457
  }
428
458
 
459
+
429
460
  if !healthcheck.nil?
430
461
  base_rrset[:health_check_id] = healthcheck
431
462
  end
@@ -445,12 +476,13 @@ module MU
445
476
 
446
477
  # Doing an UPSERT with a new set_identifier will fail with a record already exist error, so lets try and get it from an existing record.
447
478
  # This can be an issue with multiple secondary failover records
448
- if (location || failover || region || weight) && set_identifier.nil?
449
- record_sets = MU::Cloud::AWS.route53.list_resource_record_sets(
479
+ if (location || failover || region || weight) and set_identifier.nil?
480
+ record_sets ||= MU::Cloud::AWS.route53.list_resource_record_sets(
450
481
  hosted_zone_id: id,
451
482
  start_record_name: name
452
483
  ).resource_record_sets
453
484
 
485
+
454
486
  record_sets.each { |r|
455
487
  if r.name == name
456
488
  if location && location == r.location
@@ -497,18 +529,23 @@ module MU
497
529
  MU.log "Adding DNS record #{name} => #{targets} (#{type}) to #{id}", details: params
498
530
  end
499
531
 
500
- begin
532
+ return if noop
533
+
534
+ on_retry = Proc.new { |e|
535
+ if (delete and e.message.match(/but it was not found/)) or
536
+ (!delete and e.message.match(/(it|name) already exists/))
537
+ MU.log e.message, MU::DEBUG, details: params
538
+ return
539
+ elsif e.class == Aws::Route53::Errors::InvalidChangeBatch
540
+ MU.log "Problem managing entry for #{name}", MU::ERR, details: params
541
+ raise MuError, e.inspect
542
+ end
543
+ }
544
+
545
+ change_id = nil
546
+ MU.retrier([Aws::Route53::Errors::PriorRequestNotComplete, Aws::Route53::Errors::InvalidChangeBatch], wait: 15, max: 10, on_retry: on_retry) {
501
547
  change_id = MU::Cloud::AWS.route53.change_resource_record_sets(params).change_info.id
502
- rescue Aws::Route53::Errors::PriorRequestNotComplete => e
503
- sleep 10
504
- retry
505
- rescue Aws::Route53::Errors::InvalidChangeBatch, Aws::Route53::Errors::InvalidInput, Exception => e
506
- return if e.message.match(/ but it already exists/) and !delete
507
- MU.log "Failed to change DNS records, #{e.inspect}", MU::ERR, details: params
508
- raise e if !delete
509
- MU.log "Record #{name} (#{type}) in #{id} can't be deleted. Already removed? #{e.inspect}", MU::WARN, details: params if delete
510
- return
511
- end
548
+ }
512
549
 
513
550
  if sync_wait
514
551
  attempts = 0
@@ -535,23 +572,27 @@ module MU
535
572
  # @param delete [Boolean]: Remove this entry instead of creating it.
536
573
  # @param cloudclass [Object]: The resource's Mu class.
537
574
  # @param sync_wait [Boolean]: Wait for DNS entry to propagate across zone.
538
- def self.genericMuDNSEntry(name: nil, target: nil, cloudclass: nil, noop: false, delete: false, sync_wait: true)
539
- return nil if name.nil? or target.nil? or cloudclass.nil?
540
- mu_zone = MU::Cloud::DNSZone.find(cloud_id: "platform-mu").values.first
575
+ def self.genericMuDNSEntry(name: nil, target: nil, cloudclass: nil, noop: false, delete: false, sync_wait: true, credentials: nil)
576
+ return nil if name.nil? or cloudclass.nil?
577
+ return nil if target.nil? and !delete
578
+ mu_zone = MU::Cloud::DNSZone.find(cloud_id: "platform-mu", credentials: credentials).values.first
541
579
  raise MuError, "Couldn't isolate platform-mu DNS zone" if mu_zone.nil?
542
580
 
543
581
  if !mu_zone.nil? and !MU.myVPC.nil?
544
582
  subdomain = cloudclass.cfg_name
545
583
  dns_name = name.downcase+"."+subdomain
546
584
  dns_name += "."+MU.myInstanceId if MU.myInstanceId
585
+
547
586
  record_type = "CNAME"
548
587
  record_type = "A" if target.match(/^\d+\.\d+\.\d+\.\d+/)
549
588
  ip = nil
550
589
 
551
- lookup = MU::Cloud::AWS.route53.list_resource_record_sets(
552
- hosted_zone_id: mu_zone.id,
553
- start_record_name: "#{dns_name}.platform-mu",
554
- start_record_type: record_type
590
+ records = []
591
+ lookup = MU::Cloud::AWS.route53(credentials: credentials).list_resource_record_sets(
592
+ hosted_zone_id: mu_zone.id,
593
+ start_record_name: "#{dns_name}.platform-mu",
594
+ start_record_type: record_type,
595
+ max_items: 1
555
596
  ).resource_record_sets
556
597
 
557
598
  lookup.each { |record|
@@ -572,34 +613,14 @@ module MU
572
613
  # MU.log "'#{dns_name}.platform-mu' does not resolve.", MU::DEBUG, details: e.inspect
573
614
  # end
574
615
 
575
- if ip == target
576
- return "#{dns_name}.platform-mu" if !delete
577
- elsif noop
578
- return nil
616
+ if ip == target and !delete
617
+ return "#{dns_name}.platform-mu"
579
618
  end
580
619
 
581
620
  sync_wait = false if delete
582
621
 
583
622
  record_type = "R53ALIAS" if cloudclass == MU::Cloud::AWS::LoadBalancer
584
- attempts = 0
585
- begin
586
- MU::Cloud::AWS::DNSZone.manageRecord(mu_zone.id, dns_name, record_type, targets: [target], delete: delete, sync_wait: sync_wait)
587
- rescue Aws::Route53::Errors::PriorRequestNotComplete => e
588
- MU.log "Route53 was still processing a request, waiting", MU::WARN, details: e
589
- sleep 15
590
- retry
591
- rescue Aws::Route53::Errors::InvalidChangeBatch => e
592
- if e.inspect.match(/alias target name does not lie within the target zone/) and attempts < 5
593
- MU.log e.inspect, MU::WARN
594
- sleep 15
595
- attempts = attempts + 1
596
- retry
597
- elsif !e.inspect.match(/(it|name) already exists/)
598
- raise MuError, "Problem managing entry for #{dns_name} -> #{target}: #{e.inspect}"
599
- else
600
- MU.log "#{dns_name} already exists", MU::DEBUG, details: e.inspect
601
- end
602
- end
623
+ MU::Cloud::AWS::DNSZone.manageRecord(mu_zone.id, dns_name, record_type, targets: [target], delete: delete, sync_wait: sync_wait, noop: noop)
603
624
  return "#{dns_name}.platform-mu"
604
625
  else
605
626
  return nil
@@ -662,8 +683,9 @@ module MU
662
683
 
663
684
  # Called by {MU::Cleanup}. Locates resources that were created by the
664
685
  # currently-loaded deployment, and purges them.
665
- def self.cleanup(noop: false, ignoremaster: false, region: MU.curRegion, credentials: nil, flags: {})
666
- checks_to_clean = []
686
+ def self.cleanup(noop: false, deploy_id: MU.deploy_id, ignoremaster: false, region: MU.curRegion, credentials: nil, flags: {})
687
+ MU.log "AWS::DNSZone.cleanup: need to support flags['known']", MU::DEBUG, details: flags
688
+
667
689
  threads = []
668
690
  MU::Cloud::AWS.route53(credentials: credentials).list_health_checks.health_checks.each { |check|
669
691
  begin
@@ -674,7 +696,7 @@ module MU
674
696
  muid_match = false
675
697
  mumaster_match = false
676
698
  tags.each { |tag|
677
- muid_match = true if tag.key == "MU-ID" and tag.value == MU.deploy_id
699
+ muid_match = true if tag.key == "MU-ID" and tag.value == deploy_id
678
700
  mumaster_match = true if tag.key == "MU-MASTER-IP" and tag.value == MU.mu_public_ip
679
701
  }
680
702
 
@@ -692,19 +714,19 @@ module MU
692
714
  threads << Thread.new(check) { |mycheck|
693
715
  MU.dupGlobals(parent_thread_id)
694
716
  Thread.abort_on_exception = true
695
- MU.log "Removing health check #{check.id}"
717
+ MU.log "Removing health check #{mycheck.id}"
696
718
  retries = 5
697
719
  begin
698
- MU::Cloud::AWS.route53(credentials: credentials).delete_health_check(health_check_id: check.id) if !noop
720
+ MU::Cloud::AWS.route53(credentials: credentials).delete_health_check(health_mycheck_id: mycheck.id) if !noop
699
721
  rescue Aws::Route53::Errors::NoSuchHealthCheck => e
700
- MU.log "Health Check '#{check.id}' disappeared before I could remove it", MU::WARN, details: e.inspect
722
+ MU.log "Health Check '#{mycheck.id}' disappeared before I could remove it", MU::WARN, details: e.inspect
701
723
  rescue Aws::Route53::Errors::InvalidInput => e
702
724
  if e.message.match(/is still referenced from parent health check/) && retries <= 5
703
725
  sleep 5
704
726
  retries += 1
705
727
  retry
706
728
  else
707
- MU.log "Health Check #{check.id} still has a parent health check associated with it, skipping", MU::WARN, details: e.inspect
729
+ MU.log "Health Check #{mycheck.id} still has a parent health check associated with it, skipping", MU::WARN, details: e.inspect
708
730
  end
709
731
  end
710
732
  }
@@ -718,8 +740,8 @@ module MU
718
740
  t.join
719
741
  }
720
742
 
721
- zones = MU::Cloud::DNSZone.find(deploy_id: MU.deploy_id, region: region)
722
- zones.each_pair { |id, zone|
743
+ zones = MU::Cloud::DNSZone.find(deploy_id: deploy_id, region: region)
744
+ zones.values.each { |zone|
723
745
  MU.log "Purging DNS Zone '#{zone.name}' (#{zone.id})"
724
746
  if !noop
725
747
  begin
@@ -727,7 +749,6 @@ module MU
727
749
  rrsets = MU::Cloud::AWS.route53(credentials: credentials).list_resource_record_sets(hosted_zone_id: zone.id)
728
750
  rrsets.resource_record_sets.each { |rrset|
729
751
  next if zone.name == rrset.name and (rrset.type == "NS" or rrset.type == "SOA")
730
- records = []
731
752
  MU::Cloud::AWS.route53(credentials: credentials).change_resource_record_sets(
732
753
  hosted_zone_id: zone.id,
733
754
  change_batch: {
@@ -775,7 +796,7 @@ module MU
775
796
 
776
797
  # TO DO: if we have more than one record it will retry the deletion multiple times and will throw Aws::Route53::Errors::InvalidChangeBatch / record not found even though the record was deleted
777
798
  zone_rrsets.each { |record|
778
- if record.name.match(MU.deploy_id.downcase)
799
+ if record.name.match(deploy_id.downcase)
779
800
  resource_records = []
780
801
  record.resource_records.each { |rrecord|
781
802
  resource_records << rrecord.value
@@ -791,9 +812,9 @@ module MU
791
812
  end
792
813
 
793
814
  # Cloud-specific configuration properties.
794
- # @param config [MU::Config]: The calling MU::Config object
815
+ # @param _config [MU::Config]: The calling MU::Config object
795
816
  # @return [Array<Array,Hash>]: List of required fields, and json-schema Hash of cloud-specific configuration parameters for this resource
796
- def self.schema(config)
817
+ def self.schema(_config)
797
818
  toplevel_required = []
798
819
  schema = {}
799
820
  [toplevel_required, schema]
@@ -801,9 +822,9 @@ module MU
801
822
 
802
823
  # Cloud-specific pre-processing of {MU::Config::BasketofKittens::dnszones}, bare and unvalidated.
803
824
  # @param zone [Hash]: The resource to process and validate
804
- # @param configurator [MU::Config]: The overall deployment configurator of which this resource is a member
825
+ # @param _configurator [MU::Config]: The overall deployment configurator of which this resource is a member
805
826
  # @return [Boolean]: True if validation succeeded, False otherwise
806
- def self.validateConfig(zone, configurator)
827
+ def self.validateConfig(zone, _configurator)
807
828
  ok = true
808
829
 
809
830
  if !zone["records"].nil?
@@ -821,10 +842,7 @@ module MU
821
842
  end
822
843
 
823
844
  if !record['mu_type'].nil?
824
- zone["dependencies"] << {
825
- "type" => record['mu_type'],
826
- "name" => record['target']
827
- }
845
+ MU::Config.addDependency(zone, record['target'], record['mu_type'])
828
846
  end
829
847
 
830
848
  if record.has_key?('healthchecks') && !record['healthchecks'].empty?
@@ -0,0 +1,1072 @@
1
+ module MU
2
+ class Cloud
3
+ class AWS
4
+ # An API as configured in {MU::Config::BasketofKittens::endpoints}
5
+ class Endpoint < MU::Cloud::Endpoint
6
+
7
+ # Initialize this cloud resource object. Calling +super+ will invoke the initializer defined under {MU::Cloud}, which should set the attribtues listed in {MU::Cloud::PUBLIC_ATTRS} as well as applicable dependency shortcuts, like +@vpc+, for us.
8
+ # @param args [Hash]: Hash of named arguments passed via Ruby's double-splat
9
+ def initialize(**args)
10
+ super
11
+ @mu_name ||= @deploy.getResourceName(@config["name"])
12
+ end
13
+
14
+ # Called automatically by {MU::Deploy#createResources}
15
+ def create
16
+ resp = MU::Cloud::AWS.apig(region: @config['region'], credentials: @credentials).create_rest_api(
17
+ name: @mu_name,
18
+ description: @deploy.deploy_id,
19
+ endpoint_configuration: {
20
+ types: ["REGIONAL"] # XXX expose in BoK ["REGIONAL", "EDGE", "PRIVATE"]
21
+ },
22
+ tags: @tags
23
+ )
24
+ @cloud_id = resp.id
25
+ generate_methods(false)
26
+ end
27
+
28
+ # Create/update all of the methods declared for this endpoint
29
+ def generate_methods(integrations = true)
30
+ resp = MU::Cloud::AWS.apig(region: @config['region'], credentials: @credentials).get_resources(
31
+ rest_api_id: @cloud_id,
32
+ )
33
+ root_resource = resp.items.first.id
34
+
35
+ # TODO guard this crap so we don't touch it if there are no changes
36
+ @config['methods'].each { |m|
37
+ m["auth"] ||= m["iam_role"] ? "AWS_IAM" : "NONE"
38
+
39
+ method_arn = "arn:#{MU::Cloud::AWS.isGovCloud?(@config["region"]) ? "aws-us-gov" : "aws"}:execute-api:#{@config["region"]}:#{MU::Cloud::AWS.credToAcct(@credentials)}:#{@cloud_id}/*/#{m['type']}/#{m['path']}"
40
+ path_part = ["", "/"].include?(m['path']) ? nil : m['path']
41
+ method_arn.sub!(/\/\/$/, '/')
42
+
43
+ resp = MU::Cloud::AWS.apig(region: @config['region'], credentials: @credentials).get_resources(
44
+ rest_api_id: @cloud_id
45
+ )
46
+ ext_resource = nil
47
+ resp.items.each { |resource|
48
+ if resource.path_part == path_part
49
+ ext_resource = resource.id
50
+ end
51
+ }
52
+
53
+ resp = if ext_resource
54
+ MU::Cloud::AWS.apig(region: @config['region'], credentials: @credentials).get_resource(
55
+ rest_api_id: @cloud_id,
56
+ resource_id: ext_resource,
57
+ )
58
+ # MU::Cloud::AWS.apig(region: @config['region'], credentials: @credentials).update_resource(
59
+ # rest_api_id: @cloud_id,
60
+ # resource_id: ext_resource,
61
+ # patch_operations: [
62
+ # {
63
+ # op: "replace",
64
+ # path: "XXX ??",
65
+ # value: m["path"]
66
+ # }
67
+ # ]
68
+ # )
69
+ else
70
+ MU::Cloud::AWS.apig(region: @config['region'], credentials: @credentials).create_resource(
71
+ rest_api_id: @cloud_id,
72
+ parent_id: root_resource,
73
+ path_part: path_part
74
+ )
75
+ end
76
+ parent_id = resp.id
77
+
78
+ resp = begin
79
+ MU::Cloud::AWS.apig(region: @config['region'], credentials: @credentials).get_method(
80
+ rest_api_id: @cloud_id,
81
+ resource_id: parent_id,
82
+ http_method: m['type']
83
+ )
84
+ rescue Aws::APIGateway::Errors::NotFoundException
85
+ resp = MU::Cloud::AWS.apig(region: @config['region'], credentials: @credentials).put_method(
86
+ rest_api_id: @cloud_id,
87
+ resource_id: parent_id,
88
+ authorization_type: m['auth'],
89
+ http_method: m['type']
90
+ )
91
+ end
92
+
93
+ # XXX effectively a placeholder default
94
+ begin
95
+ m['responses'].each { |r|
96
+ params = {
97
+ :rest_api_id => @cloud_id,
98
+ :resource_id => parent_id,
99
+ :http_method => m['type'],
100
+ :status_code => r['code'].to_s
101
+ }
102
+ if r['headers']
103
+ params[:response_parameters] = r['headers'].map { |h|
104
+ h['required'] ||= false
105
+ ["method.response.header."+h['header'], h['required']]
106
+ }.to_h
107
+ end
108
+
109
+ if r['body']
110
+ # XXX I'm guessing we can also have arbirary user-defined models somehow, so is_error is probably inadequate to the demand of the times
111
+ params[:response_models] = r['body'].map { |b| [b['content_type'], b['is_error'] ? "Error" : "Empty"] }.to_h
112
+ end
113
+
114
+ MU::Cloud::AWS.apig(region: @config['region'], credentials: @credentials).put_method_response(params)
115
+ }
116
+ rescue Aws::APIGateway::Errors::ConflictException
117
+ # fine to ignore
118
+ end
119
+
120
+ if integrations and m['integrate_with']
121
+ # role_arn = if m['iam_role']
122
+ # if m['iam_role'].match(/^arn:/)
123
+ # m['iam_role']
124
+ # else
125
+ # sib_role = @deploy.findLitterMate(name: m['iam_role'], type: "roles")
126
+ # sib_role.cloudobj.arn
127
+ # XXX make this more like get_role_arn in Function, or just use Role.find?
128
+ # end
129
+ # end
130
+
131
+ function_obj = nil
132
+ aws_int_type = m['integrate_with']['proxy'] ? "AWS_PROXY" : "AWS"
133
+
134
+ uri, type = if m['integrate_with']['type'] == "aws_generic"
135
+ svc, action = m['integrate_with']['aws_generic_action'].split(/:/)
136
+ ["arn:aws:apigateway:"+@config['region']+":#{svc}:action/#{action}", aws_int_type]
137
+ elsif m['integrate_with']['type'] == "functions"
138
+ function_obj = nil
139
+ MU.retrier([], max: 5, wait: 9, loop_if: Proc.new { function_obj.nil? }) {
140
+ function_obj = @deploy.findLitterMate(name: m['integrate_with']['name'], type: "functions")
141
+ }
142
+ ["arn:aws:apigateway:"+@config['region']+":lambda:path/2015-03-31/functions/"+function_obj.cloudobj.arn+"/invocations", aws_int_type]
143
+ elsif m['integrate_with']['type'] == "mock"
144
+ [nil, "MOCK"]
145
+ end
146
+
147
+ params = {
148
+ :rest_api_id => @cloud_id,
149
+ :resource_id => parent_id,
150
+ :type => type, # XXX Lambda and Firehose can do AWS_PROXY
151
+ :content_handling => "CONVERT_TO_TEXT", # XXX expose in BoK
152
+ :http_method => m['type'],
153
+ :timeout_in_millis => m['timeout_in_millis']
154
+ # credentials: role_arn
155
+ }
156
+ params[:uri] = uri if uri
157
+
158
+ if m['integrate_with']['type'] != "mock"
159
+ params[:integration_http_method] = m['integrate_with']['backend_http_method']
160
+ else
161
+ params[:integration_http_method] = nil
162
+ end
163
+
164
+ if m['integrate_with']['passthrough_behavior']
165
+ params[:passthrough_behavior] = m['integrate_with']['passthrough_behavior']
166
+ end
167
+ if m['integrate_with']['request_templates']
168
+ params[:request_templates] = {}
169
+ m['integrate_with']['request_templates'].each { |rt|
170
+ params[:request_templates][rt['content_type']] = rt['template']
171
+ }
172
+ end
173
+ if m['integrate_with']['parameters']
174
+ params[:request_parameters] = Hash[m['integrate_with']['parameters'].map { |p|
175
+ ["integration.request.#{p['type']}.#{p['name']}", p['value']]
176
+ }]
177
+ end
178
+
179
+ resp = MU::Cloud::AWS.apig(region: @config['region'], credentials: @credentials).put_integration(params)
180
+
181
+ if m['integrate_with']['type'] =~ /^functions?$/
182
+ function_obj.addTrigger(method_arn, "apigateway", @config['name'])
183
+ end
184
+
185
+ m['responses'].each { |r|
186
+ params = {
187
+ :rest_api_id => @cloud_id,
188
+ :resource_id => parent_id,
189
+ :http_method => m['type'],
190
+ :status_code => r['code'].to_s,
191
+ :selection_pattern => ".*"
192
+ }
193
+ if r['headers']
194
+ params[:response_parameters] = r['headers'].map { |h|
195
+ ["method.response.header."+h['header'], "'"+h['value']+"'"]
196
+ }.to_h
197
+ end
198
+
199
+ MU::Cloud::AWS.apig(region: @config['region'], credentials: @credentials).put_integration_response(params)
200
+
201
+ }
202
+
203
+ end
204
+
205
+ }
206
+ end
207
+
208
+ # Called automatically by {MU::Deploy#createResources}
209
+ def groom
210
+ generate_methods
211
+
212
+ deployment = MU::Cloud::AWS.apig(region: @config['region'], credentials: @credentials).get_deployments(
213
+ rest_api_id: @cloud_id
214
+ ).items.sort { |a, b| a.created_date <=> b.created_date }.last
215
+
216
+ if !deployment
217
+ MU.log "Deploying API Gateway #{@config['name']} to #{@config['deploy_to']}"
218
+ deployment = MU::Cloud::AWS.apig(region: @config['region'], credentials: @credentials).create_deployment(
219
+ rest_api_id: @cloud_id,
220
+ stage_name: @config['deploy_to']
221
+ # cache_cluster_enabled: false,
222
+ # cache_cluster_size: 0.5,
223
+ )
224
+ end
225
+ # this automatically creates a stage with the same name, so we don't
226
+ # have to deal with that
227
+
228
+ my_hostname = @cloud_id+".execute-api."+@config['region']+".amazonaws.com"
229
+ my_url = "https://"+my_hostname+"/"+@config['deploy_to']
230
+ MU.log "API Endpoint #{@config['name']}: "+my_url, MU::SUMMARY
231
+
232
+ print_dns_alias = Proc.new { |rec|
233
+ rec['name'] ||= @mu_name.downcase
234
+ dnsname = MU::Cloud.resourceClass("AWS", "DNSZone").recordToName(rec)
235
+ dnsname
236
+ }
237
+
238
+ # if we have any placeholder DNS records that are intended to be
239
+ # filled out with our runtime @mu_name, do so, and add an alias if
240
+ # applicable
241
+ if @config['dns_records'] and !MU::Cloud::AWS.isGovCloud?
242
+ @config['dns_records'].each { |rec|
243
+ dnsname = print_dns_alias.call(rec)
244
+ MU.log "Alias for API Endpoint #{@config['name']}: https://"+dnsname+"/"+@config['deploy_to'], MU::SUMMARY
245
+ }
246
+ MU::Cloud.resourceClass("AWS", "DNSZone").createRecordsFromConfig(@config['dns_records'], target: my_hostname)
247
+ end
248
+
249
+ if @config['domain_names']
250
+ @config['domain_names'].each { |dom|
251
+ dnsname = if dom['dns_record']
252
+ print_dns_alias.call(dom['dns_record'])
253
+ else
254
+ dom['unmanaged_name']
255
+ end
256
+ MU.log "Alias for API Endpoint #{@config['name']}: https://"+dnsname, MU::SUMMARY
257
+
258
+ certfield, dnsfield = if dom['endpoint_type'] == "EDGE"
259
+ [:certificate_arn, :distribution_domain_name]
260
+ else
261
+ [:regional_certificate_arn, :regional_domain_name]
262
+ end
263
+
264
+ dom_desc = begin
265
+ MU::Cloud::AWS.apig(region: @config['region'], credentials: @credentials).get_domain_name(domain_name: dnsname)
266
+ rescue ::Aws::APIGateway::Errors::NotFoundException
267
+
268
+ params = {
269
+ domain_name: dnsname,
270
+ endpoint_configuration: {
271
+ types: [dom['endpoint_type']]
272
+ },
273
+ security_policy: dom['security_policy'],
274
+ tags: @tags
275
+ }
276
+ if dom['certificate']
277
+ params[certfield] = dom['certificate']['id']
278
+ end
279
+
280
+ MU.log "Creating API Gateway Domain Name #{dnsname}", MU::NOTICE, details: params
281
+ MU::Cloud::AWS.apig(region: @config['region'], credentials: @credentials).create_domain_name(params)
282
+ end
283
+
284
+ mappings = MU::Cloud::AWS.apig(region: @config['region'], credentials: @credentials).get_base_path_mappings(domain_name: dnsname, limit: 500).items
285
+ found = false
286
+ if mappings
287
+ mappings.each { |m|
288
+ if m.rest_api_id == @cloud_id and m.stage == @config['deploy_to']
289
+ found = true
290
+ break
291
+ end
292
+ }
293
+ end
294
+ if !found
295
+ MU.log "Mapping #{dnsname} to API Gateway #{@mu_name}"
296
+ MU::Cloud::AWS.apig(region: @config['region'], credentials: @credentials).create_base_path_mapping(
297
+ domain_name: dnsname,
298
+ rest_api_id: @cloud_id,
299
+ stage: @config['deploy_to']
300
+ )
301
+ end
302
+
303
+ if dom['dns_record']
304
+ MU::Cloud.resourceClass("AWS", "DNSZone").createRecordsFromConfig([dom['dns_record']], target: dom_desc.send(dnsfield))
305
+ end
306
+ }
307
+ end
308
+
309
+ # The creation of our deployment should have created a matching stage,
310
+ # which we're now going to mess with.
311
+ stage = MU::Cloud::AWS.apig(region: @config['region'], credentials: @credentials).get_stage(
312
+ rest_api_id: @cloud_id,
313
+ stage_name: @config['deploy_to']
314
+ )
315
+
316
+ if @config['access_logs'] and !stage.access_log_settings
317
+ log_ref = MU::Config::Ref.get(@config['access_logs'])
318
+ MU.log "Enabling API Gateway access logs to CloudWatch Log Group #{log_ref.cloud_id}"
319
+ stage = MU::Cloud::AWS.apig(region: @config['region'], credentials: @credentials).update_stage(
320
+ rest_api_id: @cloud_id,
321
+ stage_name: @config['deploy_to'],
322
+ patch_operations: [
323
+ {
324
+ op: "replace",
325
+ path: "/accessLogSettings/destinationArn",
326
+ value: log_ref.kitten.arn.sub(/:\*$/, '')
327
+ },
328
+ {
329
+ op: "replace",
330
+ path: "/accessLogSettings/format",
331
+ value: '$context.identity.sourceIp $context.identity.caller $context.identity.user [$context.requestTime] "$context.httpMethod $context.resourcePath $context.protocol" $context.status $context.responseLength $context.requestId'
332
+ },
333
+ {
334
+ op: "replace",
335
+ path: "/description",
336
+ value: @deploy.deploy_id
337
+ },
338
+ {
339
+ op: "replace",
340
+ path: "/*/*/logging/dataTrace",
341
+ value: "true"
342
+ },
343
+ {
344
+ op: "replace",
345
+ path: "/*/*/logging/loglevel",
346
+ value: "INFO"
347
+ }
348
+ ]
349
+ )
350
+ end
351
+
352
+
353
+ # resp = MU::Cloud::AWS.apig(region: @config['region'], credentials: @credentials).create_authorizer(
354
+ # rest_api_id: @cloud_id,
355
+ # )
356
+
357
+ # resp = MU::Cloud::AWS.apig(region: @config['region'], credentials: @credentials).create_vpc_link(
358
+ # )
359
+
360
+ end
361
+
362
+ @cloud_desc_cache = nil
363
+ # @return [Struct]
364
+ def cloud_desc(use_cache: true)
365
+ return @cloud_desc_cache if @cloud_desc_cache and use_cache
366
+ return nil if !@cloud_id
367
+ @cloud_desc_cache = MU::Cloud::AWS.apig(region: @config['region'], credentials: @credentials).get_rest_api(
368
+ rest_api_id: @cloud_id
369
+ )
370
+ @cloud_desc_cache
371
+ end
372
+
373
+ # Return the metadata for this API
374
+ # @return [Hash]
375
+ def notify
376
+ return nil if !@cloud_id or !cloud_desc(use_cache: false)
377
+ deploy_struct = MU.structToHash(cloud_desc, stringify_keys: true)
378
+ deploy_struct['url'] = "https://"+@cloud_id+".execute-api."+@config['region']+".amazonaws.com"
379
+ deploy_struct['url'] += "/"+@config['deploy_to'] if @config['deploy_to']
380
+ # XXX stages and whatnot
381
+ return deploy_struct
382
+ end
383
+
384
+ # Remove all APIs associated with the currently loaded deployment.
385
+ # @param noop [Boolean]: If true, will only print what would be done
386
+ # @param ignoremaster [Boolean]: If true, will remove resources not flagged as originating from this Mu server
387
+ # @param region [String]: The cloud provider region
388
+ # @return [void]
389
+ def self.cleanup(noop: false, deploy_id: MU.deploy_id, ignoremaster: false, region: MU.curRegion, credentials: nil, flags: {})
390
+ MU.log "AWS::Endpoint.cleanup: need to support flags['known']", MU::DEBUG, details: flags
391
+ MU.log "Placeholder: AWS Endpoint artifacts do not support tags, so ignoremaster cleanup flag has no effect", MU::DEBUG, details: ignoremaster
392
+
393
+ resp = MU::Cloud::AWS.apig(region: region, credentials: credentials).get_domain_names(limit: 500)
394
+ if resp and resp.items
395
+ resp.items.each { |d|
396
+ next if !d.tags
397
+ if d.tags["MU-ID"] == deploy_id and
398
+ (ignoremaster or d.tags["MU-MASTER-IP"] == MU.mu_public_ip)
399
+ mappings = MU::Cloud::AWS.apig(region: region, credentials: credentials).get_base_path_mappings(domain_name: d.domain_name, limit: 500).items
400
+ mappings.each { |m|
401
+ MU.log "Deleting API Gateway Domain Name mapping #{d.domain_name} => #{m.rest_api_id} path #{m.base_path}"
402
+ if !noop
403
+ MU::Cloud::AWS.apig(region: region, credentials: credentials).delete_base_path_mapping(domain_name: d.domain_name, base_path: m.base_path)
404
+ end
405
+ }
406
+ MU.log "Deleting API Gateway Domain Name #{d.domain_name}"
407
+ if !noop
408
+ MU::Cloud::AWS.apig(region: region, credentials: credentials).delete_domain_name(domain_name: d.domain_name)
409
+ end
410
+ end
411
+ }
412
+ end
413
+
414
+ resp = MU::Cloud::AWS.apig(region: region, credentials: credentials).get_rest_apis
415
+ if resp and resp.items
416
+ resp.items.each { |api|
417
+ # The stupid things don't have tags
418
+ if api.description == deploy_id
419
+ logs = MU::Cloud.resourceClass("AWS", "Log").find(region: region, credentials: credentials)
420
+ logs.each_pair { |log_id, log_desc|
421
+ if log_id =~ /^API-Gateway-Execution-Logs_#{api.id}\//
422
+ MU.log "Deleting CloudWatch Log Group #{log_id}"
423
+ if !noop
424
+ MU::Cloud::AWS.cloudwatchlogs(region: region, credentials: credentials).delete_log_group(log_group_name: log_id)
425
+ end
426
+ end
427
+ }
428
+ MU.log "Deleting API Gateway #{api.name} (#{api.id})"
429
+ if !noop
430
+ MU::Cloud::AWS.apig(region: region, credentials: credentials).delete_rest_api(
431
+ rest_api_id: api.id
432
+ )
433
+ end
434
+ end
435
+ }
436
+ end
437
+
438
+ end
439
+
440
+ # Locate an existing API.
441
+ # @return [Hash<String,OpenStruct>]: The cloud provider's complete descriptions of matching APIs.
442
+ def self.find(**args)
443
+ found = {}
444
+
445
+ if args[:cloud_id]
446
+ found[args[:cloud_id]] = MU::Cloud::AWS.apig(region: args[:region], credentials: args[:credentials]).get_rest_api(
447
+ rest_api_id: args[:cloud_id]
448
+ )
449
+ else
450
+ resp = MU::Cloud::AWS.apig(region: args[:region], credentials: args[:credentials]).get_rest_apis
451
+ if resp and resp.items
452
+ resp.items.each { |api|
453
+ found[api.id] = api
454
+ }
455
+ end
456
+ end
457
+
458
+ found
459
+ end
460
+
461
+ # Reverse-map our cloud description into a runnable config hash.
462
+ # We assume that any values we have in +@config+ are placeholders, and
463
+ # calculate our own accordingly based on what's live in the cloud.
464
+ def toKitten(**_args)
465
+ bok = {
466
+ "cloud" => "AWS",
467
+ "credentials" => @credentials,
468
+ "cloud_id" => @cloud_id,
469
+ "region" => @config['region']
470
+ }
471
+
472
+ if !cloud_desc
473
+ MU.log "toKitten failed to load a cloud_desc from #{@cloud_id}", MU::ERR, details: @config
474
+ return nil
475
+ end
476
+
477
+ bok['name'] = cloud_desc.name
478
+
479
+ resources = MU::Cloud::AWS.apig(region: @config['region'], credentials: @credentials).get_resources(
480
+ rest_api_id: @cloud_id,
481
+ ).items
482
+
483
+ resources.each { |r|
484
+ next if !r.respond_to?(:resource_methods) or r.resource_methods.nil?
485
+ r.resource_methods.each_pair { |http_type, m|
486
+ bok['methods'] ||= []
487
+ method = {}
488
+ m_desc = MU::Cloud::AWS.apig(region: @config['region'], credentials: @credentials).get_method(
489
+ rest_api_id: @cloud_id,
490
+ resource_id: r.id,
491
+ http_method: http_type
492
+ )
493
+
494
+ method['type'] = http_type
495
+ method['path'] = r.path_part || r.path
496
+ if m_desc.method_responses
497
+ m_desc.method_responses.each_pair { |code, resp_desc|
498
+ method['responses'] ||= []
499
+ resp = { "code" => code.to_i }
500
+ if resp_desc.response_parameters
501
+ resp_desc.response_parameters.each_pair { |hdr, reqd|
502
+ resp['headers'] ||= []
503
+ if hdr.match(/^method\.response\.header\.(.*)/)
504
+ resp['headers'] << {
505
+ "header" => Regexp.last_match[1],
506
+ "required" => reqd
507
+ }
508
+ else
509
+ MU.log "I don't know what to do with APIG response parameter #{hdr}", MU::ERR, details: resp_desc
510
+ end
511
+
512
+ }
513
+ end
514
+ if resp_desc.response_models
515
+ resp_desc.response_models.each_pair { |content_type, body|
516
+ resp['body'] ||= []
517
+ resp['body'] << {
518
+ "content_type" => content_type,
519
+ "is_error" => (body == "Error")
520
+ }
521
+ }
522
+
523
+ end
524
+ method['responses'] << resp
525
+
526
+ }
527
+ end
528
+
529
+ if m_desc.method_integration
530
+ if ["AWS", "AWS_PROXY"].include?(m_desc.method_integration.type)
531
+ if m_desc.method_integration.uri.match(/:lambda:path\/\d{4}-\d{2}-\d{2}\/functions\/arn:.*?:function:(.*?)\/invocations$/)
532
+ method['integrate_with'] = MU::Config::Ref.get(
533
+ id: Regexp.last_match[1],
534
+ type: "functions",
535
+ cloud: "AWS",
536
+ integration_http_method: m_desc.method_integration.http_method
537
+ )
538
+ elsif m_desc.method_integration.uri.match(/#{@config['region']}:([^:]+):action\/(.*)/)
539
+ method['integrate_with'] = {
540
+ "type" => "aws_generic",
541
+ "integration_http_method" => m_desc.method_integration.http_method,
542
+ "aws_generic_action" => Regexp.last_match[1]+":"+Regexp.last_match[2]
543
+ }
544
+ else
545
+ MU.log "I don't know what to do with #{m_desc.method_integration.uri}", MU::ERR
546
+ end
547
+ if m_desc.method_integration.http_method
548
+ method['integrate_with']['backend_http_method'] = m_desc.method_integration.http_method
549
+ end
550
+ method['proxy'] = true if m_desc.method_integration.type == "AWS_PROXY"
551
+ elsif m_desc.method_integration.type == "MOCK"
552
+ method['integrate_with'] = {
553
+ "type" => "mock"
554
+ }
555
+ else
556
+ MU.log "I don't know what to do with this integration", MU::ERR, details: m_desc.method_integration
557
+ next
558
+ end
559
+
560
+ if m_desc.method_integration.passthrough_behavior
561
+ method['integrate_with']['passthrough_behavior'] = m_desc.method_integration.passthrough_behavior
562
+ end
563
+
564
+ if m_desc.method_integration.request_templates and
565
+ !m_desc.method_integration.request_templates.empty?
566
+ method['integrate_with']['request_templates'] = m_desc.method_integration.request_templates.keys.map { |rt_content_type, template|
567
+ { "content_type" => rt_content_type, "template" => template }
568
+ }
569
+ end
570
+
571
+ if m_desc.method_integration.request_parameters
572
+ m_desc.method_integration.request_parameters.each_pair { |k, v|
573
+ if !k.match(/^integration\.request\.(header|querystring|path)\.(.*)/)
574
+ MU.log "Don't know how to handle integration request parameter '#{k}', skipping", MU::WARN
575
+ next
576
+ end
577
+ if Regexp.last_match[1] == "header" and
578
+ Regexp.last_match[2] == "X-Amz-Invocation-Type" and
579
+ v == "'Event'"
580
+ method['integrate_with']['async'] = true
581
+ else
582
+ method['integrate_with']['parameters'] ||= []
583
+ method['integrate_with']['parameters'] << {
584
+ "type" => Regexp.last_match[1],
585
+ "name" => Regexp.last_match[2],
586
+ "value" => v
587
+ }
588
+ end
589
+ }
590
+ end
591
+ end
592
+
593
+ bok['methods'] << method
594
+ }
595
+ }
596
+
597
+ deployment = MU::Cloud::AWS.apig(region: @config['region'], credentials: @credentials).get_deployments(
598
+ rest_api_id: @cloud_id
599
+ ).items.sort { |a, b| a.created_date <=> b.created_date }.last
600
+ stages = MU::Cloud::AWS.apig(region: @config['region'], credentials: @credentials).get_stages(
601
+ rest_api_id: @cloud_id,
602
+ deployment_id: deployment.id
603
+ )
604
+
605
+ # XXX we only support a single stage right now, which is a dumb
606
+ # limitation
607
+ stage = stages.item.first
608
+ if stage
609
+ bok['deploy_to'] = stage.stage_name
610
+ if stage.access_log_settings
611
+ bok['log_requests'] = true
612
+ bok['access_logs'] = MU::Config::Ref.get(
613
+ id: stage.access_log_settings.destination_arn.sub(/.*?:([^:]+)$/, '\1'),
614
+ credentials: @credentials,
615
+ region: @config['region'],
616
+ type: "logs",
617
+ cloud: "AWS"
618
+ )
619
+ end
620
+ end
621
+
622
+
623
+ bok
624
+ end
625
+
626
+ # Cloud-specific configuration properties.
627
+ # @param _config [MU::Config]: The calling MU::Config object
628
+ # @return [Array<Array,Hash>]: List of required fields, and json-schema Hash of cloud-specific configuration parameters for this resource
629
+ def self.schema(_config)
630
+ toplevel_required = []
631
+ schema = {
632
+ "domain_names" => {
633
+ "type" => "array",
634
+ "items" => {
635
+ "description" => "Configure optional Custom Domain Names to map to this API endpoint.",
636
+ "type" => "object",
637
+ "properties" => {
638
+ "certificate" => MU::Config::Ref.schema(type: "certificate", desc: "An existing IAM or ACM SSL certificate to bind to this alternate name endpoint.", omit_fields: ["cloud", "tag", "deploy_id"]),
639
+ "dns_record" => MU::Config::DNSZone.records_primitive(need_target: false, default_type: "CNAME", need_zone: true, embedded_type: "endpoint")["items"],
640
+ "unmanaged_name" => {
641
+ "type" => "string",
642
+ "description" => "If +dns_record+ is not specified, we will map this string as a domain name and assume that an external DNS record will be created pointing to us at a later time."
643
+ },
644
+ "endpoint_type" => {
645
+ "type" => "string",
646
+ "description" => "The type of endpoint to create with this domain name.",
647
+ "default" => "REGIONAL",
648
+ "enum" => ["REGIONAL", "EDGE", "PRIVATE"]
649
+ },
650
+ "security_policy" => {
651
+ "type" => "string",
652
+ "default" => "TLS_1_2",
653
+ "enum" => ["TLS_1_0", "TLS_1_2"],
654
+ "description" => "Acceptable TLS cipher suites. +TLS_1_2+ is strongly recommended."
655
+ }
656
+ }
657
+ }
658
+ },
659
+ "deploy_to" => {
660
+ "type" => "string",
661
+ "description" => "The name of an environment under which to deploy our API. If not specified, will deploy to the name of the global Mu environment for this deployment."
662
+ },
663
+ "log_requests" => {
664
+ "type" => "boolean",
665
+ "description" => "Log custom access requests to CloudWatch Logs to the log group specified by +access_logs+, as well as enabling built-in CloudWatch Logs at +INFO+ level. If +access_logs+ is unspecified, a reasonable group will be created automatically.",
666
+ "default" => true
667
+ },
668
+ "access_logs" => MU::Config::Ref.schema(type: "logs", desc: "A pre-existing or sibling Mu Cloudwatch Log group reference. If +log_requests+ is specified and this is not, a log group will be generated automatically. Setting this parameter explicitly automatically enables +log_requests+."),
669
+ "methods" => {
670
+ "items" => {
671
+ "type" => "object",
672
+ "description" => "Other cloud resources to integrate as a back end to this API Gateway",
673
+ "required" => ["integrate_with"],
674
+ "properties" => {
675
+ "integrate_with" => {
676
+ "type" => "object",
677
+ "description" => "Specify what application backend to invoke under this path/method combination",
678
+ "properties" => {
679
+ "async" => {
680
+ "type" => "boolean",
681
+ "default" => false,
682
+ "description" => "For non-proxy Lambda integrations, adds a static +X-Amz-Invocation-Type+ with value +'Event'+ to invoke the function asynchronously. See also https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-lambda-integration-async.html"
683
+ },
684
+ "parameters" => {
685
+ "type" => "array",
686
+ "items" => {
687
+ "description" => "One or headers, paths, or query string parameters to pass as request parameters to our back end. See also: https://docs.aws.amazon.com/apigateway/latest/developerguide/request-response-data-mappings.html",
688
+ "type" => "object",
689
+ "properties" => {
690
+ "name" => {
691
+ "type" => "string",
692
+ "description" => "A valid and unique integration request parameter name."
693
+ },
694
+ "value" => {
695
+ "type" => "string",
696
+ "description" => "The name of a method request parameter, or a static value contained in single quotes (+'foo'+)."
697
+ },
698
+ "type" => {
699
+ "type" => "string",
700
+ "description" => "Which HTTP artifact to use when presenting the parameter to the back end. ",
701
+ "enum" => ["header", "querystring", "path"]
702
+ }
703
+ }
704
+ }
705
+ },
706
+ "proxy" => {
707
+ "type" => "boolean",
708
+ "default" => false,
709
+ "description" => "Sets HTTP integrations to HTTP_PROXY and AWS/LAMBDA integrations to AWS_PROXY/LAMBDA_PROXY"
710
+ },
711
+ "backend_http_method" => {
712
+ "type" => "string",
713
+ "description" => "The HTTP method to use when contacting our integrated backend. If not specified, this will be set to match our front end.",
714
+ "enum" => ["GET", "POST", "PUT", "HEAD", "DELETE", "CONNECT", "OPTIONS", "TRACE"],
715
+ },
716
+ "timeout_in_millis" => {
717
+ "type" => "integer",
718
+ "description" => "Custom timeout between +50+ and +29,000+ milliseconds.",
719
+ "default" => 29000
720
+ },
721
+ "url" => {
722
+ "type" => "string",
723
+ "description" => "For HTTP or HTTP_PROXY integrations, this should be a fully-qualified URL"
724
+ },
725
+ "responses"=> {
726
+ "type" => "array",
727
+ "items" => {
728
+ "type" => "object",
729
+ "description" => "Customize the response to the client for this method, by adding headers or transforming through a template. If not specified, we will default to returning an un-transformed HTTP 200 for this method.",
730
+ "properties" => {
731
+ "code" => {
732
+ "type" => "integer",
733
+ "description" => "The HTTP status code to return",
734
+ "default" => 200
735
+ },
736
+ "headers" => {
737
+ "type" => "array",
738
+ "items" => {
739
+ "description" => "One or more headers, used by the API Gateway integration response and filtered through the method response before returning to the client",
740
+ "type" => "object",
741
+ "properties" => {
742
+ "header" => {
743
+ "type" => "string",
744
+ "description" => "The name of a header to return, such as +Access-Control-Allow-Methods+"
745
+ },
746
+ "value" => {
747
+ "type" => "string",
748
+ "description" => "The string to map to this header (ex +GET,OPTIONS+)"
749
+ },
750
+ "required" => {
751
+ "type" => "boolean",
752
+ "description" => "Indicate whether this header is required in order to return a response",
753
+ "default" => true
754
+ }
755
+ }
756
+ }
757
+ },
758
+ "body" => {
759
+ "type" => "array",
760
+ "items" => {
761
+ "type" => "object",
762
+ "description" => "Model for the body of our backend integration's response",
763
+ "properties" => {
764
+ "content_type" => {
765
+ "type" => "string",
766
+ "description" => "An HTTP content type to match to a response, such as +application/json+."
767
+ },
768
+ "is_error" => {
769
+ "type" => "boolean",
770
+ "description" => "Whether this response should be considered an error",
771
+ "default" => false
772
+ }
773
+ }
774
+ }
775
+ }
776
+ }
777
+ }
778
+ },
779
+ "arn" => {
780
+ "type" => "string",
781
+ "description" => "For AWS or AWS_PROXY integrations with a compatible Amazon resource outside of Mu, a full-qualified ARN such as `arn:aws:apigateway:us-west-2:s3:action/GetObject&Bucket=`bucket&Key=key`"
782
+ },
783
+ "name" => {
784
+ "type" => "string",
785
+ "description" => "A Mu resource name, for integrations with a sibling resource (e.g. a Function)"
786
+ },
787
+ "cors" => {
788
+ "type" => "string",
789
+ "description" => "When enabled, this will create an +OPTIONS+ method under this path with request and response header mappings that implement Cross-Origin Resource Sharing, setting +Access-Control-Allow-Origin+ to the specified value.",
790
+ },
791
+ "type" => {
792
+ "type" => "string",
793
+ "description" => "A Mu resource type, for integrations with a sibling resource (e.g. a function), or the string +aws_generic+, which we can use in combination with +aws_generic_action+ to integrate with arbitrary AWS services.",
794
+ "enum" => ["aws_generic"].concat(MU::Cloud.resource_types.values.map { |t| t[:cfg_plural] }.sort)
795
+ },
796
+ "aws_generic_action" => {
797
+ "type" => "string",
798
+ "description" => "For use when +type+ is set to +aws_generic+, this should specify the action to be performed in the style of an IAM policy action, e.g. +acm:ListCertificates+ for this integration to return a list of Certificate Manager SSL certificates."
799
+ },
800
+ "deploy_id" => {
801
+ "type" => "string",
802
+ "description" => "A Mu deploy id (e.g. DEMO-DEV-2014111400-NG), for integrations with a sibling resource (e.g. a Function)"
803
+ },
804
+ "iam_role" => {
805
+ "type" => "string",
806
+ "description" => "The name of an IAM role used to grant usage of other AWS artifacts for this integration. If not specified, we will automatically generate an appropriate role."
807
+ },
808
+ "passthrough_behavior" => {
809
+ "type" => "string",
810
+ "description" => "Specifies the pass-through behavior for incoming requests based on the +Content-Type+ header in the request, and the available mapping templates specified in +request_templates+. +WHEN_NO_MATCH+ passes the request body for unmapped content types through to the integration back end without transformation. +WHEN_NO_TEMPLATES+ allows pass-through when the integration has NO content types mapped to templates. +NEVER+ rejects unmapped content types with an HTTP +415+.",
811
+ "enum" => ["WHEN_NO_MATCH", "WHEN_NO_TEMPLATES", "NEVER"],
812
+ "default" => "WHEN_NO_MATCH"
813
+ },
814
+ "request_templates" => {
815
+ "type" => "array",
816
+ "description" => "A JSON-encoded string which represents a map of Velocity templates that are applied on the request payload based on the value of the +Content-Type+ header sent by the client. The content type value is the key in this map, and the template (as a String) is the value.",
817
+ "items" => {
818
+ "type" => "object",
819
+ "description" => "A JSON-encoded string which represents a map of Velocity templates that are applied on the request payload based on the value of the +Content-Type+ header sent by the client. The content type value is the key in this map, and the template (as a String) is the value.",
820
+ "require" => ["content_type", "template"],
821
+ "properties" => {
822
+ "content_type" => {
823
+ "type" => "string",
824
+ "description" => "An HTTP content type to match with a template, such as +application/json+."
825
+ },
826
+ "template" => {
827
+ "type" => "string",
828
+ "description" => "A Velocity template to apply to our reques payload, encoded as a one-line string, like: "+'<tt>"#set($allParams = $input.params())\\n{\\n\\"url_data_json_encoded\\":\\"$input.params(\'url\')\\"\\n}"</tt>'
829
+ }
830
+ }
831
+ }
832
+ }
833
+ }
834
+ },
835
+ "auth" => {
836
+ "type" => "string",
837
+ "enum" => ["NONE", "CUSTOM", "AWS_IAM", "COGNITO_USER_POOLS"],
838
+ "default" => "NONE"
839
+ }
840
+ }
841
+ }
842
+ }
843
+ }
844
+ [toplevel_required, schema]
845
+ end
846
+
847
+ # Does this resource type exist as a global (cloud-wide) artifact, or
848
+ # is it localized to a region/zone?
849
+ # @return [Boolean]
850
+ def self.isGlobal?
851
+ false
852
+ end
853
+
854
+ # Denote whether this resource implementation is experiment, ready for
855
+ # testing, or ready for production use.
856
+ def self.quality
857
+ MU::Cloud::BETA
858
+ end
859
+
860
+ # Canonical Amazon Resource Number for this resource
861
+ # @return [String]
862
+ def arn
863
+ "arn:#{MU::Cloud::AWS.isGovCloud?(@config["region"]) ? "aws-us-gov" : "aws"}:execute-api:#{@config["region"]}:#{MU::Cloud::AWS.credToAcct(@credentials)}:#{@cloud_id}"
864
+ end
865
+
866
+
867
+ # Cloud-specific pre-processing of {MU::Config::BasketofKittens::endpoints}, bare and unvalidated.
868
+ # @param endpoint [Hash]: The resource to process and validate
869
+ # @param configurator [MU::Config]: The overall deployment configurator of which this resource is a member
870
+ # @return [Boolean]: True if validation succeeded, False otherwise
871
+ def self.validateConfig(endpoint, configurator)
872
+ ok = true
873
+
874
+ if endpoint['log_requests'] and !endpoint['access_logs']
875
+ logdesc = {
876
+ "name" => endpoint['name']+"accesslogs",
877
+ }
878
+ logdesc["tags"] = endpoint["tags"] if endpoint['tags']
879
+ configurator.insertKitten(logdesc, "logs")
880
+ endpoint['access_logs'] = MU::Config::Ref.get(
881
+ name: endpoint['name']+"accesslogs",
882
+ type: "log",
883
+ cloud: "AWS",
884
+ credentials: endpoint['credentials'],
885
+ region: endpoint['region']
886
+ )
887
+ end
888
+
889
+ if endpoint['access_logs'] and endpoint["access_logs"]["name"]
890
+ endpoint['log_requests'] = true
891
+ MU::Config.addDependency(endpoint, endpoint["access_logs"]["name"], "log")
892
+ end
893
+
894
+ if endpoint['access_logs']
895
+ resp = MU::Cloud::AWS.apig(credentials: endpoint['credentials'], region: endpoint['region']).get_account
896
+ if !resp.cloudwatch_role_arn
897
+ MU.log "Endpoint '#{endpoint['name']}' is configured to use CloudWatch Logs, but the account-wide API Gateway log role is not configured", MU::ERR, details: "https://aws.amazon.com/premiumsupport/knowledge-center/api-gateway-cloudwatch-logs/"
898
+ ok = false
899
+ else
900
+ roles = MU::Cloud::AWS::Role.find(cloud_id: resp.cloudwatch_role_arn, credentials: endpoint['credentials'], region: endpoint['region'])
901
+ if roles.empty?
902
+ MU.log "Endpoint '#{endpoint['name']}' is configured to use CloudWatch Logs, but the configured account-wide API Gateway log role does not exist", MU::ERR, details: resp.cloudwatch_role_arn
903
+ ok = false
904
+ end
905
+ end
906
+ end
907
+
908
+ if endpoint['domain_names']
909
+ endpoint['domain_names'].each { |dom|
910
+ if dom['certificate']
911
+ cert_arn, cert_domains = MU::Cloud::AWS.resolveSSLCertificate(dom['certificate'], region: dom['region'], credentials: dom['credentials'])
912
+ if !cert_arn
913
+ MU.log "API Gateway #{endpoint['name']}: Failed to resolve SSL certificate in domain_name block", MU::ERR, details: dom
914
+ ok = false
915
+ end
916
+ end
917
+ if !dom['unmanaged_name'] and !dom['dns_record']
918
+ MU.log "API Gateway #{endpoint['name']}: Must specify either unmanaged_name or dns_record in domain_name block", MU::ERR, details: dom
919
+ ok = false
920
+ end
921
+
922
+ # Make at least an attempt to catch when we've specified the same
923
+ # DNS name to point to both the main gateway and this alternative
924
+ # endpoint, because that ish won't work. This check will miss if
925
+ # the end user specifies the zone in competing ways.
926
+ if dom['dns_record'] and endpoint['dns_records']
927
+ endpoint['dns_records'].each { |rec|
928
+ if rec['name'] == dom['dns_record']['name'] and
929
+ rec['zone'] == dom['dns_record']['zone']
930
+ MU.log "API Gateway #{endpoint['name']}: Cannot specify same entry in dns_records and domain_names", MU::ERR, details: rec
931
+ ok = false
932
+ end
933
+ }
934
+ end
935
+ }
936
+ end
937
+
938
+ append = []
939
+ endpoint['deploy_to'] ||= MU.environment || $environment || "dev"
940
+ endpoint['methods'].each { |m|
941
+ if m['integrate_with']['async']
942
+ if m['integrate_with']['type'] == "functions" and
943
+ m['integrate_with']['async']
944
+ m['integrate_with']['parameters'] ||= []
945
+ m['integrate_with']['parameters'] << {
946
+ "name" => "X-Amz-Invocation-Type",
947
+ "value" => "'Event'", # yes the single quotes are required
948
+ "type" => "header"
949
+ }
950
+ if m['integrate_with']['proxy']
951
+ MU.log "Cannot specify both of proxy and async for API Gateway method integration", MU::ERR
952
+ ok = false
953
+ end
954
+ end
955
+ end
956
+
957
+ if m['integrate_with'] and m['integrate_with']['name']
958
+ if m['integrate_with']['type'] != "aws_generic"
959
+ MU::Config.addDependency(endpoint, m['integrate_with']['name'], m['integrate_with']['type'])
960
+ end
961
+
962
+ m['integrate_with']['backend_http_method'] ||= m['type']
963
+
964
+ m['responses'] ||= [
965
+ "code" => 200
966
+ ]
967
+
968
+ if m['cors']
969
+ m['responses'].each { |r|
970
+ r['headers'] ||= []
971
+ r['headers'] << {
972
+ "header" => "Access-Control-Allow-Origin",
973
+ "value" => m['cors'],
974
+ "required" => true
975
+ }
976
+ r['headers'].uniq!
977
+ }
978
+
979
+ append << cors_option_integrations(m['path'], m['cors'])
980
+ end
981
+
982
+
983
+ if !m['iam_role']
984
+ m['uri'] ||= "*" if m['integrate_with']['type'] == "aws_generic"
985
+
986
+ roledesc = {
987
+ "name" => endpoint['name']+"-"+m['integrate_with']['name'],
988
+ "credentials" => endpoint['credentials'],
989
+ "can_assume" => [
990
+ {
991
+ "entity_id" => "apigateway.amazonaws.com",
992
+ "entity_type" => "service"
993
+ }
994
+ ],
995
+ }
996
+ if m['integrate_with']['type'] == "aws_generic"
997
+ roledesc["policies"] = [
998
+ {
999
+ "name" => m['integrate_with']['aws_generic_action'].gsub(/[^a-z]/i, ""),
1000
+ "permissions" => [m['integrate_with']['aws_generic_action']],
1001
+ "targets" => [{ "identifier" => m['uri'] }]
1002
+ }
1003
+ ]
1004
+ elsif m['integrate_with']['type'] == "functions"
1005
+ roledesc["import"] = ["AWSLambdaBasicExecutionRole"]
1006
+ end
1007
+ configurator.insertKitten(roledesc, "roles")
1008
+
1009
+ m['iam_role'] = endpoint['name']+"-"+m['integrate_with']['name']
1010
+ MU::Config.addDependency(endpoint, m['iam_role'], "role")
1011
+ end
1012
+ end
1013
+ }
1014
+ endpoint['methods'].concat(append.uniq) if endpoint['methods']
1015
+ # if something_bad
1016
+ # ok = false
1017
+ # end
1018
+
1019
+ ok
1020
+ end
1021
+
1022
+ def self.cors_option_integrations(path, origins)
1023
+ {
1024
+ "type" => "OPTIONS",
1025
+ "path" => path,
1026
+ "auth" => "NONE",
1027
+ "responses" => [
1028
+ {
1029
+ "code" => 200,
1030
+ "headers" => [
1031
+ {
1032
+ "header" => "Access-Control-Allow-Headers",
1033
+ "value" => "Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token",
1034
+ "required" => true
1035
+ },
1036
+ {
1037
+ "header" => "Access-Control-Allow-Methods",
1038
+ "value" => "GET,OPTIONS",
1039
+ "required" => true
1040
+ },
1041
+ {
1042
+ "header" => "Access-Control-Allow-Origin",
1043
+ "value" => origins,
1044
+ "required" => true
1045
+ }
1046
+ ],
1047
+ "body" => [
1048
+ {
1049
+ "content_type" => "application/json"
1050
+ }
1051
+ ]
1052
+ }
1053
+ ],
1054
+ "integrate_with" => {
1055
+ "type" => "mock",
1056
+ "passthrough_behavior" => "WHEN_NO_MATCH",
1057
+ "backend_http_method" => "OPTIONS",
1058
+ "request_templates" => [
1059
+ {
1060
+ "content_type" => "application/json",
1061
+ "template" => '{"statusCode": 200}'
1062
+ }
1063
+ ]
1064
+ }
1065
+ }
1066
+ end
1067
+ private_class_method :cors_option_integrations
1068
+
1069
+ end
1070
+ end
1071
+ end
1072
+ end