cloud-mu 3.1.6 → 3.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (181) hide show
  1. checksums.yaml +4 -4
  2. data/Dockerfile +1 -1
  3. data/bin/mu-adopt +15 -12
  4. data/bin/mu-azure-tests +57 -0
  5. data/bin/mu-cleanup +2 -4
  6. data/bin/mu-configure +37 -1
  7. data/bin/mu-deploy +3 -3
  8. data/bin/mu-findstray-tests +25 -0
  9. data/bin/mu-gen-docs +2 -4
  10. data/bin/mu-load-config.rb +2 -1
  11. data/bin/mu-run-tests +37 -12
  12. data/cloud-mu.gemspec +4 -4
  13. data/cookbooks/mu-tools/attributes/default.rb +7 -0
  14. data/cookbooks/mu-tools/libraries/helper.rb +87 -3
  15. data/cookbooks/mu-tools/recipes/apply_security.rb +39 -23
  16. data/cookbooks/mu-tools/recipes/aws_api.rb +13 -0
  17. data/cookbooks/mu-tools/recipes/google_api.rb +4 -0
  18. data/cookbooks/mu-tools/recipes/rsyslog.rb +8 -1
  19. data/cookbooks/mu-tools/resources/disk.rb +33 -12
  20. data/cookbooks/mu-tools/resources/mommacat_request.rb +1 -2
  21. data/cookbooks/mu-tools/templates/centos-8/sshd_config.erb +215 -0
  22. data/extras/clean-stock-amis +10 -2
  23. data/extras/generate-stock-images +7 -3
  24. data/extras/image-generators/AWS/centos7.yaml +19 -16
  25. data/extras/image-generators/AWS/{rhel7.yaml → rhel71.yaml} +0 -0
  26. data/extras/image-generators/AWS/{win2k12.yaml → win2k12r2.yaml} +0 -0
  27. data/modules/mommacat.ru +2 -2
  28. data/modules/mu.rb +84 -97
  29. data/modules/mu/adoption.rb +359 -59
  30. data/modules/mu/cleanup.rb +67 -44
  31. data/modules/mu/cloud.rb +108 -1754
  32. data/modules/mu/cloud/database.rb +49 -0
  33. data/modules/mu/cloud/dnszone.rb +44 -0
  34. data/modules/mu/cloud/machine_images.rb +212 -0
  35. data/modules/mu/cloud/providers.rb +81 -0
  36. data/modules/mu/cloud/resource_base.rb +929 -0
  37. data/modules/mu/cloud/server.rb +40 -0
  38. data/modules/mu/cloud/server_pool.rb +1 -0
  39. data/modules/mu/cloud/ssh_sessions.rb +228 -0
  40. data/modules/mu/cloud/winrm_sessions.rb +237 -0
  41. data/modules/mu/cloud/wrappers.rb +178 -0
  42. data/modules/mu/config.rb +122 -80
  43. data/modules/mu/config/alarm.rb +2 -6
  44. data/modules/mu/config/bucket.rb +32 -3
  45. data/modules/mu/config/cache_cluster.rb +2 -2
  46. data/modules/mu/config/cdn.rb +100 -0
  47. data/modules/mu/config/collection.rb +1 -1
  48. data/modules/mu/config/container_cluster.rb +2 -2
  49. data/modules/mu/config/database.rb +84 -105
  50. data/modules/mu/config/database.yml +1 -2
  51. data/modules/mu/config/dnszone.rb +5 -4
  52. data/modules/mu/config/doc_helpers.rb +4 -5
  53. data/modules/mu/config/endpoint.rb +2 -1
  54. data/modules/mu/config/firewall_rule.rb +3 -19
  55. data/modules/mu/config/folder.rb +1 -1
  56. data/modules/mu/config/function.rb +17 -8
  57. data/modules/mu/config/group.rb +1 -1
  58. data/modules/mu/config/habitat.rb +1 -1
  59. data/modules/mu/config/job.rb +89 -0
  60. data/modules/mu/config/loadbalancer.rb +57 -11
  61. data/modules/mu/config/log.rb +1 -1
  62. data/modules/mu/config/msg_queue.rb +1 -1
  63. data/modules/mu/config/nosqldb.rb +1 -1
  64. data/modules/mu/config/notifier.rb +8 -19
  65. data/modules/mu/config/ref.rb +81 -9
  66. data/modules/mu/config/role.rb +1 -1
  67. data/modules/mu/config/schema_helpers.rb +30 -34
  68. data/modules/mu/config/search_domain.rb +1 -1
  69. data/modules/mu/config/server.rb +5 -13
  70. data/modules/mu/config/server_pool.rb +3 -7
  71. data/modules/mu/config/storage_pool.rb +1 -1
  72. data/modules/mu/config/tail.rb +10 -0
  73. data/modules/mu/config/user.rb +1 -1
  74. data/modules/mu/config/vpc.rb +13 -17
  75. data/modules/mu/defaults/AWS.yaml +106 -106
  76. data/modules/mu/defaults/Azure.yaml +1 -0
  77. data/modules/mu/defaults/Google.yaml +1 -0
  78. data/modules/mu/deploy.rb +33 -19
  79. data/modules/mu/groomer.rb +15 -0
  80. data/modules/mu/groomers/chef.rb +3 -0
  81. data/modules/mu/logger.rb +120 -144
  82. data/modules/mu/master.rb +22 -1
  83. data/modules/mu/mommacat.rb +71 -26
  84. data/modules/mu/mommacat/daemon.rb +23 -14
  85. data/modules/mu/mommacat/naming.rb +82 -3
  86. data/modules/mu/mommacat/search.rb +59 -16
  87. data/modules/mu/mommacat/storage.rb +119 -48
  88. data/modules/mu/{clouds → providers}/README.md +1 -1
  89. data/modules/mu/{clouds → providers}/aws.rb +248 -62
  90. data/modules/mu/{clouds → providers}/aws/alarm.rb +3 -3
  91. data/modules/mu/{clouds → providers}/aws/bucket.rb +275 -41
  92. data/modules/mu/{clouds → providers}/aws/cache_cluster.rb +14 -50
  93. data/modules/mu/providers/aws/cdn.rb +782 -0
  94. data/modules/mu/{clouds → providers}/aws/collection.rb +5 -5
  95. data/modules/mu/{clouds → providers}/aws/container_cluster.rb +65 -63
  96. data/modules/mu/providers/aws/database.rb +1747 -0
  97. data/modules/mu/{clouds → providers}/aws/dnszone.rb +26 -12
  98. data/modules/mu/providers/aws/endpoint.rb +1072 -0
  99. data/modules/mu/{clouds → providers}/aws/firewall_rule.rb +39 -32
  100. data/modules/mu/{clouds → providers}/aws/folder.rb +1 -1
  101. data/modules/mu/{clouds → providers}/aws/function.rb +291 -133
  102. data/modules/mu/{clouds → providers}/aws/group.rb +18 -20
  103. data/modules/mu/{clouds → providers}/aws/habitat.rb +3 -3
  104. data/modules/mu/providers/aws/job.rb +469 -0
  105. data/modules/mu/{clouds → providers}/aws/loadbalancer.rb +77 -47
  106. data/modules/mu/{clouds → providers}/aws/log.rb +5 -5
  107. data/modules/mu/{clouds → providers}/aws/msg_queue.rb +14 -11
  108. data/modules/mu/{clouds → providers}/aws/nosqldb.rb +96 -5
  109. data/modules/mu/{clouds → providers}/aws/notifier.rb +135 -63
  110. data/modules/mu/{clouds → providers}/aws/role.rb +112 -78
  111. data/modules/mu/{clouds → providers}/aws/search_domain.rb +172 -41
  112. data/modules/mu/{clouds → providers}/aws/server.rb +120 -145
  113. data/modules/mu/{clouds → providers}/aws/server_pool.rb +42 -60
  114. data/modules/mu/{clouds → providers}/aws/storage_pool.rb +21 -38
  115. data/modules/mu/{clouds → providers}/aws/user.rb +12 -16
  116. data/modules/mu/{clouds → providers}/aws/userdata/README.md +0 -0
  117. data/modules/mu/{clouds → providers}/aws/userdata/linux.erb +5 -4
  118. data/modules/mu/{clouds → providers}/aws/userdata/windows.erb +0 -0
  119. data/modules/mu/{clouds → providers}/aws/vpc.rb +141 -73
  120. data/modules/mu/{clouds → providers}/aws/vpc_subnet.rb +0 -0
  121. data/modules/mu/{clouds → providers}/azure.rb +4 -1
  122. data/modules/mu/{clouds → providers}/azure/container_cluster.rb +1 -5
  123. data/modules/mu/{clouds → providers}/azure/firewall_rule.rb +8 -1
  124. data/modules/mu/{clouds → providers}/azure/habitat.rb +0 -0
  125. data/modules/mu/{clouds → providers}/azure/loadbalancer.rb +0 -0
  126. data/modules/mu/{clouds → providers}/azure/role.rb +0 -0
  127. data/modules/mu/{clouds → providers}/azure/server.rb +32 -24
  128. data/modules/mu/{clouds → providers}/azure/user.rb +1 -1
  129. data/modules/mu/{clouds → providers}/azure/userdata/README.md +0 -0
  130. data/modules/mu/{clouds → providers}/azure/userdata/linux.erb +0 -0
  131. data/modules/mu/{clouds → providers}/azure/userdata/windows.erb +0 -0
  132. data/modules/mu/{clouds → providers}/azure/vpc.rb +4 -6
  133. data/modules/mu/{clouds → providers}/cloudformation.rb +1 -1
  134. data/modules/mu/{clouds → providers}/cloudformation/alarm.rb +3 -3
  135. data/modules/mu/{clouds → providers}/cloudformation/cache_cluster.rb +3 -3
  136. data/modules/mu/{clouds → providers}/cloudformation/collection.rb +3 -3
  137. data/modules/mu/{clouds → providers}/cloudformation/database.rb +6 -17
  138. data/modules/mu/{clouds → providers}/cloudformation/dnszone.rb +3 -3
  139. data/modules/mu/{clouds → providers}/cloudformation/firewall_rule.rb +3 -3
  140. data/modules/mu/{clouds → providers}/cloudformation/loadbalancer.rb +3 -3
  141. data/modules/mu/{clouds → providers}/cloudformation/log.rb +3 -3
  142. data/modules/mu/{clouds → providers}/cloudformation/server.rb +7 -7
  143. data/modules/mu/{clouds → providers}/cloudformation/server_pool.rb +5 -5
  144. data/modules/mu/{clouds → providers}/cloudformation/vpc.rb +3 -3
  145. data/modules/mu/{clouds → providers}/docker.rb +0 -0
  146. data/modules/mu/{clouds → providers}/google.rb +15 -6
  147. data/modules/mu/{clouds → providers}/google/bucket.rb +2 -2
  148. data/modules/mu/{clouds → providers}/google/container_cluster.rb +29 -14
  149. data/modules/mu/{clouds → providers}/google/database.rb +2 -9
  150. data/modules/mu/{clouds → providers}/google/firewall_rule.rb +3 -3
  151. data/modules/mu/{clouds → providers}/google/folder.rb +5 -9
  152. data/modules/mu/{clouds → providers}/google/function.rb +4 -4
  153. data/modules/mu/{clouds → providers}/google/group.rb +9 -17
  154. data/modules/mu/{clouds → providers}/google/habitat.rb +4 -8
  155. data/modules/mu/{clouds → providers}/google/loadbalancer.rb +2 -2
  156. data/modules/mu/{clouds → providers}/google/role.rb +46 -35
  157. data/modules/mu/{clouds → providers}/google/server.rb +26 -11
  158. data/modules/mu/{clouds → providers}/google/server_pool.rb +11 -11
  159. data/modules/mu/{clouds → providers}/google/user.rb +32 -22
  160. data/modules/mu/{clouds → providers}/google/userdata/README.md +0 -0
  161. data/modules/mu/{clouds → providers}/google/userdata/linux.erb +0 -0
  162. data/modules/mu/{clouds → providers}/google/userdata/windows.erb +0 -0
  163. data/modules/mu/{clouds → providers}/google/vpc.rb +38 -3
  164. data/modules/tests/aws-jobs-functions.yaml +46 -0
  165. data/modules/tests/centos6.yaml +15 -0
  166. data/modules/tests/centos7.yaml +15 -0
  167. data/modules/tests/centos8.yaml +12 -0
  168. data/modules/tests/ecs.yaml +2 -2
  169. data/modules/tests/eks.yaml +1 -1
  170. data/modules/tests/functions/node-function/lambda_function.js +10 -0
  171. data/modules/tests/functions/python-function/lambda_function.py +12 -0
  172. data/modules/tests/microservice_app.yaml +288 -0
  173. data/modules/tests/rds.yaml +108 -0
  174. data/modules/tests/regrooms/rds.yaml +123 -0
  175. data/modules/tests/server-with-scrub-muisms.yaml +1 -1
  176. data/modules/tests/super_complex_bok.yml +2 -2
  177. data/modules/tests/super_simple_bok.yml +2 -2
  178. data/spec/mu/clouds/azure_spec.rb +2 -2
  179. metadata +126 -98
  180. data/modules/mu/clouds/aws/database.rb +0 -1974
  181. data/modules/mu/clouds/aws/endpoint.rb +0 -596
@@ -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?
@@ -345,7 +362,7 @@ module MU
345
362
  rescue Aws::Route53::Errors::LastVPCAssociation => e
346
363
  MU.log e.inspect, MU::WARN
347
364
  rescue Aws::Route53::Errors::VPCAssociationNotFound
348
- MU.log "VPC #{vpc_id} access to zone #{id} already revoked", MU::WARN
365
+ MU.log "VPC #{vpc_id} access to zone #{id} already revoked", MU::NOTICE
349
366
  end
350
367
  end
351
368
  end
@@ -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
@@ -825,10 +842,7 @@ module MU
825
842
  end
826
843
 
827
844
  if !record['mu_type'].nil?
828
- zone["dependencies"] << {
829
- "type" => record['mu_type'],
830
- "name" => record['target']
831
- }
845
+ MU::Config.addDependency(zone, record['target'], record['mu_type'])
832
846
  end
833
847
 
834
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