cloud-mu 3.1.5 → 3.3.2

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 (185) hide show
  1. checksums.yaml +4 -4
  2. data/Dockerfile +5 -1
  3. data/ansible/roles/mu-windows/files/LaunchConfig.json +9 -0
  4. data/ansible/roles/mu-windows/files/config.xml +76 -0
  5. data/ansible/roles/mu-windows/tasks/main.yml +16 -0
  6. data/bin/mu-adopt +16 -12
  7. data/bin/mu-azure-tests +57 -0
  8. data/bin/mu-cleanup +2 -4
  9. data/bin/mu-configure +52 -0
  10. data/bin/mu-deploy +3 -3
  11. data/bin/mu-findstray-tests +25 -0
  12. data/bin/mu-gen-docs +2 -4
  13. data/bin/mu-load-config.rb +2 -1
  14. data/bin/mu-node-manage +15 -16
  15. data/bin/mu-run-tests +37 -12
  16. data/cloud-mu.gemspec +3 -3
  17. data/cookbooks/mu-activedirectory/resources/domain.rb +4 -4
  18. data/cookbooks/mu-activedirectory/resources/domain_controller.rb +4 -4
  19. data/cookbooks/mu-tools/libraries/helper.rb +1 -1
  20. data/cookbooks/mu-tools/recipes/apply_security.rb +14 -14
  21. data/cookbooks/mu-tools/recipes/aws_api.rb +9 -0
  22. data/cookbooks/mu-tools/recipes/eks.rb +2 -2
  23. data/cookbooks/mu-tools/recipes/windows-client.rb +25 -22
  24. data/extras/clean-stock-amis +25 -19
  25. data/extras/generate-stock-images +1 -0
  26. data/extras/image-generators/AWS/win2k12.yaml +2 -0
  27. data/extras/image-generators/AWS/win2k16.yaml +2 -0
  28. data/extras/image-generators/AWS/win2k19.yaml +2 -0
  29. data/modules/mommacat.ru +1 -1
  30. data/modules/mu.rb +86 -98
  31. data/modules/mu/adoption.rb +373 -58
  32. data/modules/mu/cleanup.rb +214 -303
  33. data/modules/mu/cloud.rb +128 -1733
  34. data/modules/mu/cloud/database.rb +49 -0
  35. data/modules/mu/cloud/dnszone.rb +44 -0
  36. data/modules/mu/cloud/machine_images.rb +212 -0
  37. data/modules/mu/cloud/providers.rb +81 -0
  38. data/modules/mu/cloud/resource_base.rb +929 -0
  39. data/modules/mu/cloud/server.rb +40 -0
  40. data/modules/mu/cloud/server_pool.rb +1 -0
  41. data/modules/mu/cloud/ssh_sessions.rb +228 -0
  42. data/modules/mu/cloud/winrm_sessions.rb +237 -0
  43. data/modules/mu/cloud/wrappers.rb +169 -0
  44. data/modules/mu/config.rb +123 -81
  45. data/modules/mu/config/alarm.rb +2 -6
  46. data/modules/mu/config/bucket.rb +32 -3
  47. data/modules/mu/config/cache_cluster.rb +2 -2
  48. data/modules/mu/config/cdn.rb +100 -0
  49. data/modules/mu/config/collection.rb +1 -1
  50. data/modules/mu/config/container_cluster.rb +7 -2
  51. data/modules/mu/config/database.rb +84 -105
  52. data/modules/mu/config/database.yml +1 -2
  53. data/modules/mu/config/dnszone.rb +5 -4
  54. data/modules/mu/config/doc_helpers.rb +5 -6
  55. data/modules/mu/config/endpoint.rb +2 -1
  56. data/modules/mu/config/firewall_rule.rb +3 -19
  57. data/modules/mu/config/folder.rb +1 -1
  58. data/modules/mu/config/function.rb +17 -8
  59. data/modules/mu/config/group.rb +1 -1
  60. data/modules/mu/config/habitat.rb +1 -1
  61. data/modules/mu/config/job.rb +89 -0
  62. data/modules/mu/config/loadbalancer.rb +57 -11
  63. data/modules/mu/config/log.rb +1 -1
  64. data/modules/mu/config/msg_queue.rb +1 -1
  65. data/modules/mu/config/nosqldb.rb +1 -1
  66. data/modules/mu/config/notifier.rb +8 -19
  67. data/modules/mu/config/ref.rb +92 -14
  68. data/modules/mu/config/role.rb +1 -1
  69. data/modules/mu/config/schema_helpers.rb +38 -37
  70. data/modules/mu/config/search_domain.rb +1 -1
  71. data/modules/mu/config/server.rb +12 -13
  72. data/modules/mu/config/server_pool.rb +3 -7
  73. data/modules/mu/config/storage_pool.rb +1 -1
  74. data/modules/mu/config/tail.rb +11 -0
  75. data/modules/mu/config/user.rb +1 -1
  76. data/modules/mu/config/vpc.rb +27 -23
  77. data/modules/mu/config/vpc.yml +0 -1
  78. data/modules/mu/defaults/AWS.yaml +90 -90
  79. data/modules/mu/defaults/Azure.yaml +1 -0
  80. data/modules/mu/defaults/Google.yaml +1 -0
  81. data/modules/mu/deploy.rb +34 -20
  82. data/modules/mu/groomer.rb +16 -1
  83. data/modules/mu/groomers/ansible.rb +69 -4
  84. data/modules/mu/groomers/chef.rb +51 -4
  85. data/modules/mu/logger.rb +120 -144
  86. data/modules/mu/master.rb +97 -4
  87. data/modules/mu/mommacat.rb +160 -874
  88. data/modules/mu/mommacat/daemon.rb +23 -14
  89. data/modules/mu/mommacat/naming.rb +110 -3
  90. data/modules/mu/mommacat/search.rb +497 -0
  91. data/modules/mu/mommacat/storage.rb +252 -194
  92. data/modules/mu/{clouds → providers}/README.md +1 -1
  93. data/modules/mu/{clouds → providers}/aws.rb +258 -57
  94. data/modules/mu/{clouds → providers}/aws/alarm.rb +3 -3
  95. data/modules/mu/{clouds → providers}/aws/bucket.rb +275 -41
  96. data/modules/mu/{clouds → providers}/aws/cache_cluster.rb +14 -50
  97. data/modules/mu/providers/aws/cdn.rb +782 -0
  98. data/modules/mu/{clouds → providers}/aws/collection.rb +5 -5
  99. data/modules/mu/{clouds → providers}/aws/container_cluster.rb +95 -84
  100. data/modules/mu/providers/aws/database.rb +1744 -0
  101. data/modules/mu/{clouds → providers}/aws/dnszone.rb +26 -12
  102. data/modules/mu/providers/aws/endpoint.rb +1072 -0
  103. data/modules/mu/{clouds → providers}/aws/firewall_rule.rb +39 -32
  104. data/modules/mu/{clouds → providers}/aws/folder.rb +1 -1
  105. data/modules/mu/{clouds → providers}/aws/function.rb +289 -134
  106. data/modules/mu/{clouds → providers}/aws/group.rb +18 -20
  107. data/modules/mu/{clouds → providers}/aws/habitat.rb +3 -3
  108. data/modules/mu/providers/aws/job.rb +466 -0
  109. data/modules/mu/{clouds → providers}/aws/loadbalancer.rb +77 -47
  110. data/modules/mu/{clouds → providers}/aws/log.rb +5 -5
  111. data/modules/mu/{clouds → providers}/aws/msg_queue.rb +14 -11
  112. data/modules/mu/{clouds → providers}/aws/nosqldb.rb +96 -5
  113. data/modules/mu/{clouds → providers}/aws/notifier.rb +135 -63
  114. data/modules/mu/{clouds → providers}/aws/role.rb +76 -48
  115. data/modules/mu/{clouds → providers}/aws/search_domain.rb +172 -41
  116. data/modules/mu/{clouds → providers}/aws/server.rb +66 -98
  117. data/modules/mu/{clouds → providers}/aws/server_pool.rb +42 -60
  118. data/modules/mu/{clouds → providers}/aws/storage_pool.rb +21 -38
  119. data/modules/mu/{clouds → providers}/aws/user.rb +12 -16
  120. data/modules/mu/{clouds → providers}/aws/userdata/README.md +0 -0
  121. data/modules/mu/{clouds → providers}/aws/userdata/linux.erb +5 -4
  122. data/modules/mu/{clouds → providers}/aws/userdata/windows.erb +0 -0
  123. data/modules/mu/{clouds → providers}/aws/vpc.rb +143 -74
  124. data/modules/mu/{clouds → providers}/aws/vpc_subnet.rb +0 -0
  125. data/modules/mu/{clouds → providers}/azure.rb +13 -0
  126. data/modules/mu/{clouds → providers}/azure/container_cluster.rb +1 -5
  127. data/modules/mu/{clouds → providers}/azure/firewall_rule.rb +8 -1
  128. data/modules/mu/{clouds → providers}/azure/habitat.rb +0 -0
  129. data/modules/mu/{clouds → providers}/azure/loadbalancer.rb +0 -0
  130. data/modules/mu/{clouds → providers}/azure/role.rb +0 -0
  131. data/modules/mu/{clouds → providers}/azure/server.rb +32 -24
  132. data/modules/mu/{clouds → providers}/azure/user.rb +1 -1
  133. data/modules/mu/{clouds → providers}/azure/userdata/README.md +0 -0
  134. data/modules/mu/{clouds → providers}/azure/userdata/linux.erb +0 -0
  135. data/modules/mu/{clouds → providers}/azure/userdata/windows.erb +0 -0
  136. data/modules/mu/{clouds → providers}/azure/vpc.rb +4 -6
  137. data/modules/mu/{clouds → providers}/cloudformation.rb +10 -0
  138. data/modules/mu/{clouds → providers}/cloudformation/alarm.rb +3 -3
  139. data/modules/mu/{clouds → providers}/cloudformation/cache_cluster.rb +3 -3
  140. data/modules/mu/{clouds → providers}/cloudformation/collection.rb +3 -3
  141. data/modules/mu/{clouds → providers}/cloudformation/database.rb +6 -17
  142. data/modules/mu/{clouds → providers}/cloudformation/dnszone.rb +3 -3
  143. data/modules/mu/{clouds → providers}/cloudformation/firewall_rule.rb +3 -3
  144. data/modules/mu/{clouds → providers}/cloudformation/loadbalancer.rb +3 -3
  145. data/modules/mu/{clouds → providers}/cloudformation/log.rb +3 -3
  146. data/modules/mu/{clouds → providers}/cloudformation/server.rb +7 -7
  147. data/modules/mu/{clouds → providers}/cloudformation/server_pool.rb +5 -5
  148. data/modules/mu/{clouds → providers}/cloudformation/vpc.rb +3 -3
  149. data/modules/mu/{clouds → providers}/docker.rb +0 -0
  150. data/modules/mu/{clouds → providers}/google.rb +29 -6
  151. data/modules/mu/{clouds → providers}/google/bucket.rb +4 -4
  152. data/modules/mu/{clouds → providers}/google/container_cluster.rb +38 -20
  153. data/modules/mu/{clouds → providers}/google/database.rb +5 -12
  154. data/modules/mu/{clouds → providers}/google/firewall_rule.rb +5 -5
  155. data/modules/mu/{clouds → providers}/google/folder.rb +5 -9
  156. data/modules/mu/{clouds → providers}/google/function.rb +6 -6
  157. data/modules/mu/{clouds → providers}/google/group.rb +9 -17
  158. data/modules/mu/{clouds → providers}/google/habitat.rb +4 -8
  159. data/modules/mu/{clouds → providers}/google/loadbalancer.rb +5 -5
  160. data/modules/mu/{clouds → providers}/google/role.rb +50 -31
  161. data/modules/mu/{clouds → providers}/google/server.rb +41 -24
  162. data/modules/mu/{clouds → providers}/google/server_pool.rb +14 -14
  163. data/modules/mu/{clouds → providers}/google/user.rb +34 -24
  164. data/modules/mu/{clouds → providers}/google/userdata/README.md +0 -0
  165. data/modules/mu/{clouds → providers}/google/userdata/linux.erb +0 -0
  166. data/modules/mu/{clouds → providers}/google/userdata/windows.erb +0 -0
  167. data/modules/mu/{clouds → providers}/google/vpc.rb +45 -14
  168. data/modules/tests/aws-jobs-functions.yaml +46 -0
  169. data/modules/tests/centos6.yaml +15 -0
  170. data/modules/tests/centos7.yaml +15 -0
  171. data/modules/tests/centos8.yaml +12 -0
  172. data/modules/tests/ecs.yaml +2 -2
  173. data/modules/tests/eks.yaml +1 -1
  174. data/modules/tests/functions/node-function/lambda_function.js +10 -0
  175. data/modules/tests/functions/python-function/lambda_function.py +12 -0
  176. data/modules/tests/microservice_app.yaml +288 -0
  177. data/modules/tests/rds.yaml +108 -0
  178. data/modules/tests/regrooms/rds.yaml +123 -0
  179. data/modules/tests/server-with-scrub-muisms.yaml +1 -1
  180. data/modules/tests/super_complex_bok.yml +2 -2
  181. data/modules/tests/super_simple_bok.yml +3 -5
  182. data/spec/mu/clouds/azure_spec.rb +2 -2
  183. metadata +122 -92
  184. data/modules/mu/clouds/aws/database.rb +0 -1974
  185. 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