cloud-mu 3.2.0 → 3.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. checksums.yaml +4 -4
  2. data/Dockerfile +1 -1
  3. data/bin/mu-adopt +12 -1
  4. data/bin/mu-load-config.rb +2 -1
  5. data/bin/mu-run-tests +14 -2
  6. data/cloud-mu.gemspec +3 -3
  7. data/modules/mu.rb +2 -2
  8. data/modules/mu/adoption.rb +5 -5
  9. data/modules/mu/cleanup.rb +47 -25
  10. data/modules/mu/cloud.rb +29 -1
  11. data/modules/mu/cloud/dnszone.rb +0 -2
  12. data/modules/mu/cloud/resource_base.rb +9 -3
  13. data/modules/mu/cloud/wrappers.rb +4 -0
  14. data/modules/mu/config.rb +1 -1
  15. data/modules/mu/config/bucket.rb +31 -2
  16. data/modules/mu/config/cache_cluster.rb +1 -1
  17. data/modules/mu/config/cdn.rb +100 -0
  18. data/modules/mu/config/container_cluster.rb +1 -1
  19. data/modules/mu/config/database.rb +1 -1
  20. data/modules/mu/config/dnszone.rb +4 -3
  21. data/modules/mu/config/endpoint.rb +1 -0
  22. data/modules/mu/config/function.rb +16 -7
  23. data/modules/mu/config/job.rb +89 -0
  24. data/modules/mu/config/notifier.rb +7 -18
  25. data/modules/mu/config/ref.rb +53 -7
  26. data/modules/mu/config/server.rb +1 -1
  27. data/modules/mu/config/vpc.rb +1 -0
  28. data/modules/mu/defaults/AWS.yaml +26 -26
  29. data/modules/mu/deploy.rb +13 -0
  30. data/modules/mu/master.rb +21 -0
  31. data/modules/mu/mommacat.rb +1 -0
  32. data/modules/mu/mommacat/daemon.rb +13 -7
  33. data/modules/mu/providers/aws.rb +115 -16
  34. data/modules/mu/providers/aws/alarm.rb +2 -2
  35. data/modules/mu/providers/aws/bucket.rb +274 -40
  36. data/modules/mu/providers/aws/cache_cluster.rb +4 -4
  37. data/modules/mu/providers/aws/cdn.rb +782 -0
  38. data/modules/mu/providers/aws/collection.rb +2 -2
  39. data/modules/mu/providers/aws/container_cluster.rb +57 -37
  40. data/modules/mu/providers/aws/database.rb +11 -11
  41. data/modules/mu/providers/aws/dnszone.rb +24 -7
  42. data/modules/mu/providers/aws/endpoint.rb +535 -50
  43. data/modules/mu/providers/aws/firewall_rule.rb +6 -3
  44. data/modules/mu/providers/aws/folder.rb +1 -1
  45. data/modules/mu/providers/aws/function.rb +288 -125
  46. data/modules/mu/providers/aws/group.rb +9 -7
  47. data/modules/mu/providers/aws/habitat.rb +2 -2
  48. data/modules/mu/providers/aws/job.rb +466 -0
  49. data/modules/mu/providers/aws/loadbalancer.rb +9 -8
  50. data/modules/mu/providers/aws/log.rb +3 -3
  51. data/modules/mu/providers/aws/msg_queue.rb +12 -3
  52. data/modules/mu/providers/aws/nosqldb.rb +96 -5
  53. data/modules/mu/providers/aws/notifier.rb +135 -63
  54. data/modules/mu/providers/aws/role.rb +51 -37
  55. data/modules/mu/providers/aws/search_domain.rb +165 -29
  56. data/modules/mu/providers/aws/server.rb +12 -9
  57. data/modules/mu/providers/aws/server_pool.rb +26 -13
  58. data/modules/mu/providers/aws/storage_pool.rb +2 -2
  59. data/modules/mu/providers/aws/user.rb +4 -4
  60. data/modules/mu/providers/aws/userdata/linux.erb +5 -4
  61. data/modules/mu/providers/aws/vpc.rb +3 -3
  62. data/modules/mu/providers/azure/server.rb +2 -1
  63. data/modules/mu/providers/google.rb +1 -0
  64. data/modules/mu/providers/google/bucket.rb +1 -1
  65. data/modules/mu/providers/google/container_cluster.rb +1 -1
  66. data/modules/mu/providers/google/database.rb +1 -1
  67. data/modules/mu/providers/google/firewall_rule.rb +1 -1
  68. data/modules/mu/providers/google/folder.rb +1 -1
  69. data/modules/mu/providers/google/function.rb +1 -1
  70. data/modules/mu/providers/google/group.rb +1 -1
  71. data/modules/mu/providers/google/habitat.rb +1 -1
  72. data/modules/mu/providers/google/loadbalancer.rb +1 -1
  73. data/modules/mu/providers/google/role.rb +4 -2
  74. data/modules/mu/providers/google/server.rb +1 -1
  75. data/modules/mu/providers/google/server_pool.rb +1 -1
  76. data/modules/mu/providers/google/user.rb +1 -1
  77. data/modules/mu/providers/google/vpc.rb +1 -1
  78. data/modules/tests/aws-jobs-functions.yaml +46 -0
  79. data/modules/tests/centos6.yaml +4 -0
  80. data/modules/tests/centos7.yaml +4 -0
  81. data/modules/tests/ecs.yaml +2 -2
  82. data/modules/tests/eks.yaml +1 -1
  83. data/modules/tests/functions/node-function/lambda_function.js +10 -0
  84. data/modules/tests/functions/python-function/lambda_function.py +12 -0
  85. data/modules/tests/microservice_app.yaml +288 -0
  86. data/modules/tests/rds.yaml +5 -5
  87. data/modules/tests/regrooms/rds.yaml +5 -5
  88. data/modules/tests/server-with-scrub-muisms.yaml +1 -1
  89. data/modules/tests/super_complex_bok.yml +2 -2
  90. data/modules/tests/super_simple_bok.yml +2 -2
  91. metadata +12 -4
@@ -242,7 +242,7 @@ module MU
242
242
  # @param region [String]: The cloud provider region
243
243
  # @param wait [Boolean]: Block on the removal of this stack; AWS deletion will continue in the background otherwise if false.
244
244
  # @return [void]
245
- def self.cleanup(noop: false, ignoremaster: false, region: MU.curRegion, wait: false, credentials: nil, flags: {})
245
+ def self.cleanup(noop: false, deploy_id: MU.deploy_id, ignoremaster: false, region: MU.curRegion, wait: false, credentials: nil, flags: {})
246
246
  MU.log "AWS::Collection.cleanup: need to support flags['known']", MU::DEBUG, details: flags
247
247
  MU.log "Placeholder: AWS Collection artifacts do not support tags, so ignoremaster cleanup flag has no effect", MU::DEBUG, details: ignoremaster
248
248
 
@@ -251,7 +251,7 @@ module MU
251
251
  resp.stacks.each { |stack|
252
252
  ok = false
253
253
  stack.tags.each { |tag|
254
- ok = true if (tag.key == "MU-ID") and tag.value == MU.deploy_id
254
+ ok = true if (tag.key == "MU-ID") and tag.value == deploy_id
255
255
  }
256
256
  if ok
257
257
  MU.log "Deleting CloudFormation stack #{stack.stack_name})"
@@ -287,6 +287,7 @@ MU.log c.name, MU::NOTICE, details: t
287
287
  # @return [OpenStruct]
288
288
  def cloud_desc(use_cache: true)
289
289
  return @cloud_desc_cache if @cloud_desc_cache and use_cache
290
+ return nil if !@cloud_id
290
291
  @cloud_desc_cache = if @config['flavor'] == "EKS" or
291
292
  (@config['flavor'] == "Fargate" and !@config['containers'])
292
293
  resp = MU::Cloud::AWS.eks(region: @config['region'], credentials: @config['credentials']).describe_cluster(
@@ -326,7 +327,7 @@ MU.log c.name, MU::NOTICE, details: t
326
327
  end
327
328
 
328
329
  @@eks_versions = {}
329
- @@eks_version_semaphore = Mutex.new
330
+ @@eks_version_semaphores = {}
330
331
  # Use the AWS SSM API to fetch the current version of the Amazon Linux
331
332
  # ECS-optimized AMI, so we can use it as a default AMI for ECS deploys.
332
333
  # @param flavor [String]: ECS or EKS
@@ -339,24 +340,22 @@ MU.log c.name, MU::NOTICE, details: t
339
340
  names: ["/aws/service/#{flavor.downcase}/optimized-ami/amazon-linux/recommended"]
340
341
  )
341
342
  else
342
- @@eks_version_semaphore.synchronize {
343
+ @@eks_version_semaphores[region] ||= Mutex.new
344
+
345
+ @@eks_version_semaphores[region].synchronize {
343
346
  if !@@eks_versions[region]
344
347
  @@eks_versions[region] ||= []
345
348
  versions = {}
346
- resp = nil
347
- next_token = nil
348
- begin
349
- resp = MU::Cloud::AWS.ssm(region: region).get_parameters_by_path(
350
- path: "/aws/service/#{flavor.downcase}",
351
- recursive: true,
352
- next_token: next_token
353
- )
354
- resp.parameters.each { |p|
355
- p.name.match(/\/aws\/service\/eks\/optimized-ami\/([^\/]+?)\//)
356
- versions[Regexp.last_match[1]] = true
357
- }
358
- next_token = resp.next_token
359
- end while !next_token.nil?
349
+ resp = MU::Cloud::AWS.ssm(region: region).get_parameters_by_path(
350
+ path: "/aws/service/#{flavor.downcase}/optimized-ami",
351
+ recursive: true,
352
+ max_results: 10 # as high as it goes, ugh
353
+ )
354
+
355
+ resp.parameters.each { |p|
356
+ p.name.match(/\/aws\/service\/eks\/optimized-ami\/([^\/]+?)\//)
357
+ versions[Regexp.last_match[1]] = true
358
+ }
360
359
  @@eks_versions[region] = versions.keys.sort { |a, b| MU.version_sort(a, b) }
361
360
  end
362
361
  }
@@ -376,16 +375,31 @@ MU.log c.name, MU::NOTICE, details: t
376
375
  nil
377
376
  end
378
377
 
378
+ @@supported_eks_region_cache = []
379
+ @@eks_region_semaphore = Mutex.new
380
+
379
381
  # Return the list of regions where we know EKS is supported.
380
- def self.EKSRegions(credentials = nil, region: nil)
381
- eks_regions = []
382
- check_regions = region ? [region] : MU::Cloud::AWS.listRegions(credentials: credentials)
383
- check_regions.each { |r|
384
- ami = getStandardImage("EKS", r)
385
- eks_regions << r if ami
386
- }
382
+ def self.EKSRegions(credentials = nil)
383
+ @@eks_region_semaphore.synchronize {
384
+ if @@supported_eks_region_cache and !@@supported_eks_region_cache.empty?
385
+ return @@supported_eks_region_cache
386
+ end
387
+ start = Time.now
388
+ # the SSM API is painfully slow for large result sets, so thread
389
+ # these and do them in parallel
390
+ @@supported_eks_region_cache = []
391
+ region_threads = []
392
+ MU::Cloud::AWS.listRegions(credentials: credentials).each { |region|
393
+ region_threads << Thread.new(region) { |r|
394
+ r_start = Time.now
395
+ ami = getStandardImage("EKS", r)
396
+ @@supported_eks_region_cache << r if ami
397
+ }
398
+ }
399
+ region_threads.each { |t| t.join }
387
400
 
388
- eks_regions
401
+ @@supported_eks_region_cache
402
+ }
389
403
  end
390
404
 
391
405
  # Does this resource type exist as a global (cloud-wide) artifact, or
@@ -406,30 +420,32 @@ MU.log c.name, MU::NOTICE, details: t
406
420
  # @param ignoremaster [Boolean]: If true, will remove resources not flagged as originating from this Mu server
407
421
  # @param region [String]: The cloud provider region
408
422
  # @return [void]
409
- def self.cleanup(noop: false, ignoremaster: false, region: MU.curRegion, credentials: nil, flags: {})
423
+ def self.cleanup(noop: false, deploy_id: MU.deploy_id, ignoremaster: false, region: MU.curRegion, credentials: nil, flags: {})
410
424
  MU.log "AWS::ContainerCluster.cleanup: need to support flags['known']", MU::DEBUG, details: flags
411
425
  MU.log "Placeholder: AWS ContainerCluster artifacts do not support tags, so ignoremaster cleanup flag has no effect", MU::DEBUG, details: ignoremaster
412
426
 
413
- purge_ecs_clusters(noop: noop, region: region, credentials: credentials)
427
+ purge_ecs_clusters(noop: noop, region: region, credentials: credentials, deploy_id: deploy_id)
414
428
 
415
- purge_eks_clusters(noop: noop, region: region, credentials: credentials)
429
+ purge_eks_clusters(noop: noop, region: region, credentials: credentials, deploy_id: deploy_id)
416
430
 
417
431
  end
418
432
 
419
- def self.purge_eks_clusters(noop: false, region: MU.curRegion, credentials: nil)
420
- return if !MU::Cloud::AWS::ContainerCluster.EKSRegions(credentials, region: region).include?(region)
433
+ def self.purge_eks_clusters(noop: false, region: MU.curRegion, credentials: nil, deploy_id: MU.deploy_id)
421
434
  resp = begin
422
435
  MU::Cloud::AWS.eks(credentials: credentials, region: region).list_clusters
423
436
  rescue Aws::EKS::Errors::AccessDeniedException
424
437
  # EKS isn't actually live in this region, even though SSM lists
425
438
  # base images for it
439
+ if @@supported_eks_region_cache
440
+ @@supported_eks_region_cache.delete(region)
441
+ end
426
442
  return
427
443
  end
428
444
 
429
445
  return if !resp or !resp.clusters
430
446
 
431
447
  resp.clusters.each { |cluster|
432
- if cluster.match(/^#{MU.deploy_id}-/)
448
+ if cluster.match(/^#{deploy_id}-/)
433
449
 
434
450
  desc = MU::Cloud::AWS.eks(credentials: credentials, region: region).describe_cluster(
435
451
  name: cluster
@@ -473,13 +489,14 @@ MU.log c.name, MU::NOTICE, details: t
473
489
  end
474
490
  private_class_method :purge_eks_clusters
475
491
 
476
- def self.purge_ecs_clusters(noop: false, region: MU.curRegion, credentials: nil)
492
+ def self.purge_ecs_clusters(noop: false, region: MU.curRegion, credentials: nil, deploy_id: MU.deploy_id)
493
+ start = Time.now
477
494
  resp = MU::Cloud::AWS.ecs(credentials: credentials, region: region).list_clusters
478
495
 
479
496
  return if !resp or !resp.cluster_arns or resp.cluster_arns.empty?
480
497
 
481
498
  resp.cluster_arns.each { |arn|
482
- if arn.match(/:cluster\/(#{MU.deploy_id}[^:]+)$/)
499
+ if arn.match(/:cluster\/(#{deploy_id}[^:]+)$/)
483
500
  cluster = Regexp.last_match[1]
484
501
 
485
502
  svc_resp = MU::Cloud::AWS.ecs(region: region, credentials: credentials).list_services(
@@ -525,7 +542,7 @@ MU.log c.name, MU::NOTICE, details: t
525
542
  }
526
543
 
527
544
  tasks = MU::Cloud::AWS.ecs(region: region, credentials: credentials).list_task_definitions(
528
- family_prefix: MU.deploy_id
545
+ family_prefix: deploy_id
529
546
  )
530
547
 
531
548
  if tasks and tasks.task_definition_arns
@@ -1221,12 +1238,12 @@ start = Time.now
1221
1238
 
1222
1239
  cluster["flavor"] = "EKS" if cluster["flavor"].match(/^Kubernetes$/i)
1223
1240
 
1224
- if cluster["flavor"] == "ECS" and cluster["kubernetes"] and !MU::Cloud::AWS.isGovCloud?(cluster["region"]) and !cluster["containers"] and MU::Cloud::AWS::ContainerCluster.EKSRegions(cluster['credentials'], region: cluster['region']).include?(cluster['region'])
1241
+ if cluster["flavor"] == "ECS" and cluster["kubernetes"] and !MU::Cloud::AWS.isGovCloud?(cluster["region"]) and !cluster["containers"] and MU::Cloud::AWS::ContainerCluster.EKSRegions(cluster['credentials']).include?(cluster['region'])
1225
1242
  cluster["flavor"] = "EKS"
1226
1243
  MU.log "Setting flavor of ContainerCluster '#{cluster['name']}' to EKS ('kubernetes' stanza was specified)", MU::NOTICE
1227
1244
  end
1228
1245
 
1229
- if cluster["flavor"] == "EKS" and !MU::Cloud::AWS::ContainerCluster.EKSRegions(cluster['credentials'], region: cluster['region']).include?(cluster['region'])
1246
+ if cluster["flavor"] == "EKS" and !MU::Cloud::AWS::ContainerCluster.EKSRegions(cluster['credentials']).include?(cluster['region'])
1230
1247
  MU.log "EKS is only available in some regions", MU::ERR, details: MU::Cloud::AWS::ContainerCluster.EKSRegions
1231
1248
  ok = false
1232
1249
  end
@@ -1484,7 +1501,8 @@ start = Time.now
1484
1501
  worker_pool[k] = cluster[k]
1485
1502
  end
1486
1503
  }
1487
-
1504
+ else
1505
+ worker_pool["groom"] = false # don't meddle with ECS workers unnecessarily
1488
1506
  end
1489
1507
 
1490
1508
  configurator.insertKitten(worker_pool, "server_pools")
@@ -1731,7 +1749,7 @@ start = Time.now
1731
1749
  @deploy.findLitterMate(type: "server_pools", name: @config["name"]+"workers")
1732
1750
  end
1733
1751
  serverpool.listNodes.each { |mynode|
1734
- resources = resource_lookup[node.cloud_desc.instance_type]
1752
+ resources = resource_lookup[mynode.cloud_desc.instance_type]
1735
1753
  threads << Thread.new(mynode) { |node|
1736
1754
  ident_doc = nil
1737
1755
  ident_doc_sig = nil
@@ -1932,6 +1950,8 @@ start = Time.now
1932
1950
  task_params[:network_mode] = "awsvpc"
1933
1951
  task_params[:cpu] = cpu_total.to_i.to_s
1934
1952
  task_params[:memory] = mem_total.to_i.to_s
1953
+ elsif @config['vpc']
1954
+ task_params[:network_mode] = "awsvpc"
1935
1955
  end
1936
1956
 
1937
1957
  MU.log "Registering task definition #{service_name} with #{container_definitions.size.to_s} containers"
@@ -680,7 +680,7 @@ dependencies
680
680
  # Return the metadata for this ContainerCluster
681
681
  # @return [Hash]
682
682
  def notify
683
- deploy_struct = MU.structToHash(cloud_desc)
683
+ deploy_struct = MU.structToHash(cloud_desc, stringify_keys: true)
684
684
  deploy_struct['cloud_id'] = @cloud_id
685
685
  deploy_struct["region"] ||= @config['region']
686
686
  deploy_struct["db_name"] ||= @config['db_name']
@@ -761,7 +761,7 @@ dependencies
761
761
  end
762
762
 
763
763
  # @return [Array<Thread>]
764
- def self.threaded_resource_purge(describe_method, list_method, id_method, arn_type, region, credentials, ignoremaster, known: [])
764
+ def self.threaded_resource_purge(describe_method, list_method, id_method, arn_type, region, credentials, ignoremaster, known: [], deploy_id: MU.deploy_id)
765
765
  deletia = []
766
766
 
767
767
  resp = MU::Cloud::AWS.rds(credentials: credentials, region: region).send(describe_method)
@@ -774,7 +774,7 @@ dependencies
774
774
  next
775
775
  end
776
776
 
777
- if should_delete?(tags, resource.send(id_method), ignoremaster, MU.deploy_id, MU.mu_public_ip, known)
777
+ if should_delete?(tags, resource.send(id_method), ignoremaster, deploy_id, MU.mu_public_ip, known)
778
778
  deletia << resource.send(id_method)
779
779
  end
780
780
  }
@@ -795,18 +795,18 @@ dependencies
795
795
  # @param ignoremaster [Boolean]: If true, will remove resources not flagged as originating from this Mu server
796
796
  # @param region [String]: The cloud provider region in which to operate
797
797
  # @return [void]
798
- def self.cleanup(noop: false, ignoremaster: false, credentials: nil, region: MU.curRegion, flags: {})
798
+ def self.cleanup(noop: false, deploy_id: MU.deploy_id, ignoremaster: false, credentials: nil, region: MU.curRegion, flags: {})
799
799
 
800
800
  ["instance", "cluster"].each { |type|
801
- threaded_resource_purge("describe_db_#{type}s".to_sym, "db_#{type}s".to_sym, "db_#{type}_identifier".to_sym, (type == "instance" ? "db" : "cluster"), region, credentials, ignoremaster, known: flags['known']) { |id|
802
- terminate_rds_instance(nil, noop: noop, skipsnapshots: flags["skipsnapshots"], region: region, deploy_id: MU.deploy_id, cloud_id: id, mu_name: id.upcase, credentials: credentials, cluster: (type == "cluster"), known: flags['known'])
801
+ threaded_resource_purge("describe_db_#{type}s".to_sym, "db_#{type}s".to_sym, "db_#{type}_identifier".to_sym, (type == "instance" ? "db" : "cluster"), region, credentials, ignoremaster, known: flags['known'], deploy_id: deploy_id) { |id|
802
+ terminate_rds_instance(nil, noop: noop, skipsnapshots: flags["skipsnapshots"], region: region, deploy_id: deploy_id, cloud_id: id, mu_name: id.upcase, credentials: credentials, cluster: (type == "cluster"), known: flags['known'])
803
803
 
804
804
  }.each { |t|
805
805
  t.join
806
806
  }
807
807
  }
808
808
 
809
- threads = threaded_resource_purge(:describe_db_subnet_groups, :db_subnet_groups, :db_subnet_group_name, "subgrp", region, credentials, ignoremaster, known: flags['known']) { |id|
809
+ threads = threaded_resource_purge(:describe_db_subnet_groups, :db_subnet_groups, :db_subnet_group_name, "subgrp", region, credentials, ignoremaster, known: flags['known'], deploy_id: deploy_id) { |id|
810
810
  MU.log "Deleting RDS subnet group #{id}"
811
811
  MU.retrier([Aws::RDS::Errors::InvalidDBSubnetGroupStateFault], wait: 30, max: 5, ignoreme: [Aws::RDS::Errors::DBSubnetGroupNotFoundFault]) {
812
812
  MU::Cloud::AWS.rds(region: region).delete_db_subnet_group(db_subnet_group_name: id) if !noop
@@ -814,7 +814,7 @@ dependencies
814
814
  }
815
815
 
816
816
  ["db", "db_cluster"].each { |type|
817
- threads.concat threaded_resource_purge("describe_#{type}_parameter_groups".to_sym, "#{type}_parameter_groups".to_sym, "#{type}_parameter_group_name".to_sym, (type == "db" ? "pg" : "cluster-pg"), region, credentials, ignoremaster, known: flags['known']) { |id|
817
+ threads.concat threaded_resource_purge("describe_#{type}_parameter_groups".to_sym, "#{type}_parameter_groups".to_sym, "#{type}_parameter_group_name".to_sym, (type == "db" ? "pg" : "cluster-pg"), region, credentials, ignoremaster, known: flags['known'], deploy_id: deploy_id) { |id|
818
818
  MU.log "Deleting RDS #{type} parameter group #{id}"
819
819
  MU.retrier([Aws::RDS::Errors::InvalidDBParameterGroupState], wait: 30, max: 5, ignoreme: [Aws::RDS::Errors::DBParameterGroupNotFound]) {
820
820
  MU::Cloud::AWS.rds(region: region).send("delete_#{type}_parameter_group", { "#{type}_parameter_group_name".to_sym => id }) if !noop
@@ -1274,7 +1274,7 @@ dependencies
1274
1274
 
1275
1275
 
1276
1276
  def add_cluster_node
1277
- cluster = MU::Config::Ref.get(@config["member_of_cluster"]).kitten(@deploy, debug: true)
1277
+ cluster = MU::Config::Ref.get(@config["member_of_cluster"]).kitten(@deploy)
1278
1278
  if cluster.nil? or cluster.cloud_id.nil?
1279
1279
  raise MuError.new "Failed to resolve parent cluster of #{@mu_name}", details: @config["member_of_cluster"].to_h
1280
1280
  end
@@ -1355,7 +1355,7 @@ dependencies
1355
1355
 
1356
1356
  # creation_style = point_in_time
1357
1357
  def create_point_in_time
1358
- @config["source"].kitten(@deploy, debug: true)
1358
+ @config["source"].kitten(@deploy)
1359
1359
  if !@config["source"].id
1360
1360
  raise MuError.new "Database '#{@config['name']}' couldn't resolve cloud id for source database", details: @config["source"].to_h
1361
1361
  end
@@ -1381,7 +1381,7 @@ dependencies
1381
1381
 
1382
1382
  # creation_style = new, existing and read_replica_of is not nil
1383
1383
  def create_read_replica
1384
- @config["source"].kitten(@deploy, debug: true)
1384
+ @config["source"].kitten(@deploy)
1385
1385
  if !@config["source"].id
1386
1386
  raise MuError.new "Database '#{@config['name']}' couldn't resolve cloud id for source database", details: @config["source"].to_h
1387
1387
  end
@@ -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?
@@ -666,7 +683,7 @@ module MU
666
683
 
667
684
  # Called by {MU::Cleanup}. Locates resources that were created by the
668
685
  # currently-loaded deployment, and purges them.
669
- def self.cleanup(noop: false, ignoremaster: false, region: MU.curRegion, credentials: nil, flags: {})
686
+ def self.cleanup(noop: false, deploy_id: MU.deploy_id, ignoremaster: false, region: MU.curRegion, credentials: nil, flags: {})
670
687
  MU.log "AWS::DNSZone.cleanup: need to support flags['known']", MU::DEBUG, details: flags
671
688
 
672
689
  threads = []
@@ -679,7 +696,7 @@ module MU
679
696
  muid_match = false
680
697
  mumaster_match = false
681
698
  tags.each { |tag|
682
- 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
683
700
  mumaster_match = true if tag.key == "MU-MASTER-IP" and tag.value == MU.mu_public_ip
684
701
  }
685
702
 
@@ -723,7 +740,7 @@ module MU
723
740
  t.join
724
741
  }
725
742
 
726
- zones = MU::Cloud::DNSZone.find(deploy_id: MU.deploy_id, region: region)
743
+ zones = MU::Cloud::DNSZone.find(deploy_id: deploy_id, region: region)
727
744
  zones.values.each { |zone|
728
745
  MU.log "Purging DNS Zone '#{zone.name}' (#{zone.id})"
729
746
  if !noop
@@ -779,7 +796,7 @@ module MU
779
796
 
780
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
781
798
  zone_rrsets.each { |record|
782
- if record.name.match(MU.deploy_id.downcase)
799
+ if record.name.match(deploy_id.downcase)
783
800
  resource_records = []
784
801
  record.resource_records.each { |rrecord|
785
802
  resource_records << rrecord.value
@@ -13,22 +13,21 @@ module MU
13
13
 
14
14
  # Called automatically by {MU::Deploy#createResources}
15
15
  def create
16
- resp = MU::Cloud::AWS.apig(region: @config['region'], credentials: @config['credentials']).create_rest_api(
16
+ resp = MU::Cloud::AWS.apig(region: @config['region'], credentials: @credentials).create_rest_api(
17
17
  name: @mu_name,
18
18
  description: @deploy.deploy_id,
19
19
  endpoint_configuration: {
20
20
  types: ["REGIONAL"] # XXX expose in BoK ["REGIONAL", "EDGE", "PRIVATE"]
21
- }
21
+ },
22
+ tags: @tags
22
23
  )
23
24
  @cloud_id = resp.id
24
- generate_methods
25
-
26
-
25
+ generate_methods(false)
27
26
  end
28
27
 
29
28
  # Create/update all of the methods declared for this endpoint
30
- def generate_methods
31
- resp = MU::Cloud::AWS.apig(region: @config['region'], credentials: @config['credentials']).get_resources(
29
+ def generate_methods(integrations = true)
30
+ resp = MU::Cloud::AWS.apig(region: @config['region'], credentials: @credentials).get_resources(
32
31
  rest_api_id: @cloud_id,
33
32
  )
34
33
  root_resource = resp.items.first.id
@@ -37,24 +36,26 @@ module MU
37
36
  @config['methods'].each { |m|
38
37
  m["auth"] ||= m["iam_role"] ? "AWS_IAM" : "NONE"
39
38
 
40
- method_arn = "arn:#{MU::Cloud::AWS.isGovCloud?(@config["region"]) ? "aws-us-gov" : "aws"}:execute-api:#{@config["region"]}:#{MU::Cloud::AWS.credToAcct(@config['credentials'])}:#{@cloud_id}/*/#{m['type']}/#{m['path']}"
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!(/\/\/$/, '/')
41
42
 
42
- resp = MU::Cloud::AWS.apig(region: @config['region'], credentials: @config['credentials']).get_resources(
43
+ resp = MU::Cloud::AWS.apig(region: @config['region'], credentials: @credentials).get_resources(
43
44
  rest_api_id: @cloud_id
44
45
  )
45
46
  ext_resource = nil
46
47
  resp.items.each { |resource|
47
- if resource.path_part == m['path']
48
+ if resource.path_part == path_part
48
49
  ext_resource = resource.id
49
50
  end
50
51
  }
51
52
 
52
53
  resp = if ext_resource
53
- MU::Cloud::AWS.apig(region: @config['region'], credentials: @config['credentials']).get_resource(
54
+ MU::Cloud::AWS.apig(region: @config['region'], credentials: @credentials).get_resource(
54
55
  rest_api_id: @cloud_id,
55
56
  resource_id: ext_resource,
56
57
  )
57
- # MU::Cloud::AWS.apig(region: @config['region'], credentials: @config['credentials']).update_resource(
58
+ # MU::Cloud::AWS.apig(region: @config['region'], credentials: @credentials).update_resource(
58
59
  # rest_api_id: @cloud_id,
59
60
  # resource_id: ext_resource,
60
61
  # patch_operations: [
@@ -66,22 +67,22 @@ MU::Cloud::AWS.apig(region: @config['region'], credentials: @config['credentials
66
67
  # ]
67
68
  # )
68
69
  else
69
- MU::Cloud::AWS.apig(region: @config['region'], credentials: @config['credentials']).create_resource(
70
+ MU::Cloud::AWS.apig(region: @config['region'], credentials: @credentials).create_resource(
70
71
  rest_api_id: @cloud_id,
71
72
  parent_id: root_resource,
72
- path_part: m['path']
73
+ path_part: path_part
73
74
  )
74
75
  end
75
76
  parent_id = resp.id
76
77
 
77
78
  resp = begin
78
- MU::Cloud::AWS.apig(region: @config['region'], credentials: @config['credentials']).get_method(
79
+ MU::Cloud::AWS.apig(region: @config['region'], credentials: @credentials).get_method(
79
80
  rest_api_id: @cloud_id,
80
81
  resource_id: parent_id,
81
82
  http_method: m['type']
82
83
  )
83
84
  rescue Aws::APIGateway::Errors::NotFoundException
84
- resp = MU::Cloud::AWS.apig(region: @config['region'], credentials: @config['credentials']).put_method(
85
+ resp = MU::Cloud::AWS.apig(region: @config['region'], credentials: @credentials).put_method(
85
86
  rest_api_id: @cloud_id,
86
87
  resource_id: parent_id,
87
88
  authorization_type: m['auth'],
@@ -100,6 +101,7 @@ MU::Cloud::AWS.apig(region: @config['region'], credentials: @config['credentials
100
101
  }
101
102
  if r['headers']
102
103
  params[:response_parameters] = r['headers'].map { |h|
104
+ h['required'] ||= false
103
105
  ["method.response.header."+h['header'], h['required']]
104
106
  }.to_h
105
107
  end
@@ -109,13 +111,13 @@ MU::Cloud::AWS.apig(region: @config['region'], credentials: @config['credentials
109
111
  params[:response_models] = r['body'].map { |b| [b['content_type'], b['is_error'] ? "Error" : "Empty"] }.to_h
110
112
  end
111
113
 
112
- MU::Cloud::AWS.apig(region: @config['region'], credentials: @config['credentials']).put_method_response(params)
114
+ MU::Cloud::AWS.apig(region: @config['region'], credentials: @credentials).put_method_response(params)
113
115
  }
114
116
  rescue Aws::APIGateway::Errors::ConflictException
115
117
  # fine to ignore
116
118
  end
117
119
 
118
- if m['integrate_with']
120
+ if integrations and m['integrate_with']
119
121
  # role_arn = if m['iam_role']
120
122
  # if m['iam_role'].match(/^arn:/)
121
123
  # m['iam_role']
@@ -127,13 +129,17 @@ MU::Cloud::AWS.apig(region: @config['region'], credentials: @config['credentials
127
129
  # end
128
130
 
129
131
  function_obj = nil
132
+ aws_int_type = m['integrate_with']['proxy'] ? "AWS_PROXY" : "AWS"
130
133
 
131
134
  uri, type = if m['integrate_with']['type'] == "aws_generic"
132
135
  svc, action = m['integrate_with']['aws_generic_action'].split(/:/)
133
- ["arn:aws:apigateway:"+@config['region']+":#{svc}:action/#{action}", "AWS"]
134
- elsif m['integrate_with']['type'] == "function"
135
- function_obj = @deploy.findLitterMate(name: m['integrate_with']['name'], type: "functions").cloudobj
136
- ["arn:aws:apigateway:"+@config['region']+":lambda:path/2015-03-31/functions/"+function_obj.arn+"/invocations", "AWS"]
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]
137
143
  elsif m['integrate_with']['type'] == "mock"
138
144
  [nil, "MOCK"]
139
145
  end
@@ -143,7 +149,8 @@ MU::Cloud::AWS.apig(region: @config['region'], credentials: @config['credentials
143
149
  :resource_id => parent_id,
144
150
  :type => type, # XXX Lambda and Firehose can do AWS_PROXY
145
151
  :content_handling => "CONVERT_TO_TEXT", # XXX expose in BoK
146
- :http_method => m['type']
152
+ :http_method => m['type'],
153
+ :timeout_in_millis => m['timeout_in_millis']
147
154
  # credentials: role_arn
148
155
  }
149
156
  params[:uri] = uri if uri
@@ -163,10 +170,15 @@ MU::Cloud::AWS.apig(region: @config['region'], credentials: @config['credentials
163
170
  params[:request_templates][rt['content_type']] = rt['template']
164
171
  }
165
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
166
178
 
167
- resp = MU::Cloud::AWS.apig(region: @config['region'], credentials: @config['credentials']).put_integration(params)
179
+ resp = MU::Cloud::AWS.apig(region: @config['region'], credentials: @credentials).put_integration(params)
168
180
 
169
- if m['integrate_with']['type'] == "function"
181
+ if m['integrate_with']['type'] =~ /^functions?$/
170
182
  function_obj.addTrigger(method_arn, "apigateway", @config['name'])
171
183
  end
172
184
 
@@ -176,7 +188,7 @@ MU::Cloud::AWS.apig(region: @config['region'], credentials: @config['credentials
176
188
  :resource_id => parent_id,
177
189
  :http_method => m['type'],
178
190
  :status_code => r['code'].to_s,
179
- :selection_pattern => ""
191
+ :selection_pattern => ".*"
180
192
  }
181
193
  if r['headers']
182
194
  params[:response_parameters] = r['headers'].map { |h|
@@ -184,7 +196,7 @@ MU::Cloud::AWS.apig(region: @config['region'], credentials: @config['credentials
184
196
  }.to_h
185
197
  end
186
198
 
187
- MU::Cloud::AWS.apig(region: @config['region'], credentials: @config['credentials']).put_integration_response(params)
199
+ MU::Cloud::AWS.apig(region: @config['region'], credentials: @credentials).put_integration_response(params)
188
200
 
189
201
  }
190
202
 
@@ -197,24 +209,152 @@ MU::Cloud::AWS.apig(region: @config['region'], credentials: @config['credentials
197
209
  def groom
198
210
  generate_methods
199
211
 
200
- MU.log "Deploying API Gateway #{@config['name']} to #{@config['deploy_to']}"
201
- MU::Cloud::AWS.apig(region: @config['region'], credentials: @config['credentials']).create_deployment(
202
- rest_api_id: @cloud_id,
203
- stage_name: @config['deploy_to']
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']
204
221
  # cache_cluster_enabled: false,
205
222
  # cache_cluster_size: 0.5,
206
- )
223
+ )
224
+ end
207
225
  # this automatically creates a stage with the same name, so we don't
208
226
  # have to deal with that
209
227
 
210
- my_url = "https://"+@cloud_id+".execute-api."+@config['region']+".amazonaws.com/"+@config['deploy_to']
228
+ my_hostname = @cloud_id+".execute-api."+@config['region']+".amazonaws.com"
229
+ my_url = "https://"+my_hostname+"/"+@config['deploy_to']
211
230
  MU.log "API Endpoint #{@config['name']}: "+my_url, MU::SUMMARY
212
231
 
213
- # resp = MU::Cloud::AWS.apig(region: @config['region'], credentials: @config['credentials']).create_authorizer(
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(
214
354
  # rest_api_id: @cloud_id,
215
355
  # )
216
356
 
217
- # resp = MU::Cloud::AWS.apig(region: @config['region'], credentials: @config['credentials']).create_vpc_link(
357
+ # resp = MU::Cloud::AWS.apig(region: @config['region'], credentials: @credentials).create_vpc_link(
218
358
  # )
219
359
 
220
360
  end
@@ -223,7 +363,8 @@ MU::Cloud::AWS.apig(region: @config['region'], credentials: @config['credentials
223
363
  # @return [Struct]
224
364
  def cloud_desc(use_cache: true)
225
365
  return @cloud_desc_cache if @cloud_desc_cache and use_cache
226
- @cloud_desc_cache = MU::Cloud::AWS.apig(region: @config['region'], credentials: @config['credentials']).get_rest_api(
366
+ return nil if !@cloud_id
367
+ @cloud_desc_cache = MU::Cloud::AWS.apig(region: @config['region'], credentials: @credentials).get_rest_api(
227
368
  rest_api_id: @cloud_id
228
369
  )
229
370
  @cloud_desc_cache
@@ -232,7 +373,10 @@ MU::Cloud::AWS.apig(region: @config['region'], credentials: @config['credentials
232
373
  # Return the metadata for this API
233
374
  # @return [Hash]
234
375
  def notify
235
- deploy_struct = MU.structToHash(cloud_desc)
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']
236
380
  # XXX stages and whatnot
237
381
  return deploy_struct
238
382
  end
@@ -242,15 +386,45 @@ MU::Cloud::AWS.apig(region: @config['region'], credentials: @config['credentials
242
386
  # @param ignoremaster [Boolean]: If true, will remove resources not flagged as originating from this Mu server
243
387
  # @param region [String]: The cloud provider region
244
388
  # @return [void]
245
- def self.cleanup(noop: false, ignoremaster: false, region: MU.curRegion, credentials: nil, flags: {})
389
+ def self.cleanup(noop: false, deploy_id: MU.deploy_id, ignoremaster: false, region: MU.curRegion, credentials: nil, flags: {})
246
390
  MU.log "AWS::Endpoint.cleanup: need to support flags['known']", MU::DEBUG, details: flags
247
391
  MU.log "Placeholder: AWS Endpoint artifacts do not support tags, so ignoremaster cleanup flag has no effect", MU::DEBUG, details: ignoremaster
248
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
+
249
414
  resp = MU::Cloud::AWS.apig(region: region, credentials: credentials).get_rest_apis
250
415
  if resp and resp.items
251
416
  resp.items.each { |api|
252
417
  # The stupid things don't have tags
253
- if api.description == MU.deploy_id
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
+ }
254
428
  MU.log "Deleting API Gateway #{api.name} (#{api.id})"
255
429
  if !noop
256
430
  MU::Cloud::AWS.apig(region: region, credentials: credentials).delete_rest_api(
@@ -260,6 +434,7 @@ MU::Cloud::AWS.apig(region: @config['region'], credentials: @config['credentials
260
434
  end
261
435
  }
262
436
  end
437
+
263
438
  end
264
439
 
265
440
  # Locate an existing API.
@@ -283,16 +458,214 @@ MU::Cloud::AWS.apig(region: @config['region'], credentials: @config['credentials
283
458
  found
284
459
  end
285
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
+
286
626
  # Cloud-specific configuration properties.
287
627
  # @param _config [MU::Config]: The calling MU::Config object
288
628
  # @return [Array<Array,Hash>]: List of required fields, and json-schema Hash of cloud-specific configuration parameters for this resource
289
629
  def self.schema(_config)
290
630
  toplevel_required = []
291
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
+ },
292
659
  "deploy_to" => {
293
660
  "type" => "string",
294
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."
295
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+."),
296
669
  "methods" => {
297
670
  "items" => {
298
671
  "type" => "object",
@@ -303,16 +676,48 @@ MU::Cloud::AWS.apig(region: @config['region'], credentials: @config['credentials
303
676
  "type" => "object",
304
677
  "description" => "Specify what application backend to invoke under this path/method combination",
305
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
+ },
306
706
  "proxy" => {
307
707
  "type" => "boolean",
308
708
  "default" => false,
309
- "description" => "For HTTP or AWS integrations, specify whether the target is a proxy (((docs unclear, is that actually what this means?)))" # XXX is that actually what this means?
709
+ "description" => "Sets HTTP integrations to HTTP_PROXY and AWS/LAMBDA integrations to AWS_PROXY/LAMBDA_PROXY"
310
710
  },
311
711
  "backend_http_method" => {
312
712
  "type" => "string",
313
713
  "description" => "The HTTP method to use when contacting our integrated backend. If not specified, this will be set to match our front end.",
314
714
  "enum" => ["GET", "POST", "PUT", "HEAD", "DELETE", "CONNECT", "OPTIONS", "TRACE"],
315
715
  },
716
+ "timeout_in_millis" => {
717
+ "type" => "integer",
718
+ "description" => "Custom timeout between +50+ and +29,000+ milliseconds.",
719
+ "default" => 29000
720
+ },
316
721
  "url" => {
317
722
  "type" => "string",
318
723
  "description" => "For HTTP or HTTP_PROXY integrations, this should be a fully-qualified URL"
@@ -380,14 +785,13 @@ MU::Cloud::AWS.apig(region: @config['region'], credentials: @config['credentials
380
785
  "description" => "A Mu resource name, for integrations with a sibling resource (e.g. a Function)"
381
786
  },
382
787
  "cors" => {
383
- "type" => "boolean",
384
- "description" => "When enabled, this will create an +OPTIONS+ method under this path with request and response header mappings that implement Cross-Origin Resource Sharing",
385
- "default" => true
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.",
386
790
  },
387
791
  "type" => {
388
792
  "type" => "string",
389
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.",
390
- "enum" => ["aws_generic"].concat(MU::Cloud.resource_types.values.map { |t| t[:cfg_name] }.sort)
794
+ "enum" => ["aws_generic"].concat(MU::Cloud.resource_types.values.map { |t| t[:cfg_plural] }.sort)
391
795
  },
392
796
  "aws_generic_action" => {
393
797
  "type" => "string",
@@ -456,7 +860,7 @@ MU::Cloud::AWS.apig(region: @config['region'], credentials: @config['credentials
456
860
  # Canonical Amazon Resource Number for this resource
457
861
  # @return [String]
458
862
  def arn
459
- "arn:#{MU::Cloud::AWS.isGovCloud?(@config["region"]) ? "aws-us-gov" : "aws"}:execute-api:#{@config["region"]}:#{MU::Cloud::AWS.credToAcct(@config['credentials'])}:#{@cloud_id}"
863
+ "arn:#{MU::Cloud::AWS.isGovCloud?(@config["region"]) ? "aws-us-gov" : "aws"}:execute-api:#{@config["region"]}:#{MU::Cloud::AWS.credToAcct(@credentials)}:#{@cloud_id}"
460
864
  end
461
865
 
462
866
 
@@ -467,9 +871,89 @@ MU::Cloud::AWS.apig(region: @config['region'], credentials: @config['credentials
467
871
  def self.validateConfig(endpoint, configurator)
468
872
  ok = true
469
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
+
470
938
  append = []
471
939
  endpoint['deploy_to'] ||= MU.environment || $environment || "dev"
472
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
+
473
957
  if m['integrate_with'] and m['integrate_with']['name']
474
958
  if m['integrate_with']['type'] != "aws_generic"
475
959
  MU::Config.addDependency(endpoint, m['integrate_with']['name'], m['integrate_with']['type'])
@@ -486,15 +970,16 @@ MU::Cloud::AWS.apig(region: @config['region'], credentials: @config['credentials
486
970
  r['headers'] ||= []
487
971
  r['headers'] << {
488
972
  "header" => "Access-Control-Allow-Origin",
489
- "value" => "*",
973
+ "value" => m['cors'],
490
974
  "required" => true
491
975
  }
492
976
  r['headers'].uniq!
493
977
  }
494
978
 
495
- append << cors_option_integrations(m['path'])
979
+ append << cors_option_integrations(m['path'], m['cors'])
496
980
  end
497
981
 
982
+
498
983
  if !m['iam_role']
499
984
  m['uri'] ||= "*" if m['integrate_with']['type'] == "aws_generic"
500
985
 
@@ -516,7 +1001,7 @@ MU::Cloud::AWS.apig(region: @config['region'], credentials: @config['credentials
516
1001
  "targets" => [{ "identifier" => m['uri'] }]
517
1002
  }
518
1003
  ]
519
- elsif m['integrate_with']['type'] == "function"
1004
+ elsif m['integrate_with']['type'] == "functions"
520
1005
  roledesc["import"] = ["AWSLambdaBasicExecutionRole"]
521
1006
  end
522
1007
  configurator.insertKitten(roledesc, "roles")
@@ -534,7 +1019,7 @@ MU::Cloud::AWS.apig(region: @config['region'], credentials: @config['credentials
534
1019
  ok
535
1020
  end
536
1021
 
537
- def self.cors_option_integrations(path)
1022
+ def self.cors_option_integrations(path, origins)
538
1023
  {
539
1024
  "type" => "OPTIONS",
540
1025
  "path" => path,
@@ -555,7 +1040,7 @@ MU::Cloud::AWS.apig(region: @config['region'], credentials: @config['credentials
555
1040
  },
556
1041
  {
557
1042
  "header" => "Access-Control-Allow-Origin",
558
- "value" => "*",
1043
+ "value" => origins,
559
1044
  "required" => true
560
1045
  }
561
1046
  ],