cloud-mu 2.0.0.pre.alpha9 → 2.0.0.pre.beta1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (61) hide show
  1. checksums.yaml +4 -4
  2. data/Berksfile.lock +1 -1
  3. data/README.md +2 -0
  4. data/bin/mu-configure +2 -58
  5. data/bin/mu-gen-docs +29 -4
  6. data/bin/mu-load-config.rb +0 -1
  7. data/bin/mu-user-manage +4 -0
  8. data/cloud-mu.gemspec +2 -2
  9. data/cookbooks/mu-master/recipes/default.rb +3 -4
  10. data/cookbooks/mu-master/recipes/init.rb +3 -3
  11. data/cookbooks/mu-tools/files/default/Mu_CA.pem +15 -15
  12. data/cookbooks/mu-tools/libraries/helper.rb +1 -1
  13. data/cookbooks/mu-tools/recipes/eks.rb +3 -3
  14. data/cookbooks/mu-tools/recipes/set_local_fw.rb +1 -1
  15. data/cookbooks/mu-utility/recipes/remi.rb +1 -1
  16. data/cookbooks/nagios/libraries/base.rb +4 -4
  17. data/cookbooks/nagios/libraries/contact.rb +1 -1
  18. data/cookbooks/nagios/libraries/contactgroup.rb +1 -1
  19. data/cookbooks/nagios/libraries/host.rb +2 -2
  20. data/cookbooks/nagios/libraries/hostdependency.rb +3 -3
  21. data/cookbooks/nagios/libraries/hostescalation.rb +3 -3
  22. data/cookbooks/nagios/libraries/hostgroup.rb +2 -2
  23. data/cookbooks/nagios/libraries/nagios.rb +5 -5
  24. data/cookbooks/nagios/libraries/service.rb +3 -3
  25. data/cookbooks/nagios/libraries/servicedependency.rb +2 -2
  26. data/cookbooks/nagios/libraries/serviceescalation.rb +2 -2
  27. data/cookbooks/nagios/libraries/servicegroup.rb +2 -2
  28. data/cookbooks/nagios/libraries/timeperiod.rb +1 -1
  29. data/install/installer +1 -1
  30. data/modules/mu/cleanup.rb +1 -1
  31. data/modules/mu/cloud.rb +43 -1
  32. data/modules/mu/clouds/aws.rb +55 -35
  33. data/modules/mu/clouds/aws/bucket.rb +287 -0
  34. data/modules/mu/clouds/aws/database.rb +65 -11
  35. data/modules/mu/clouds/aws/endpoint.rb +592 -0
  36. data/modules/mu/clouds/aws/firewall_rule.rb +4 -0
  37. data/modules/mu/clouds/aws/function.rb +138 -93
  38. data/modules/mu/clouds/aws/nosqldb.rb +387 -0
  39. data/modules/mu/clouds/aws/role.rb +1 -1
  40. data/modules/mu/clouds/aws/server.rb +5 -5
  41. data/modules/mu/clouds/aws/server_pool.rb +60 -3
  42. data/modules/mu/clouds/azure.rb +0 -1
  43. data/modules/mu/clouds/google.rb +34 -12
  44. data/modules/mu/clouds/google/bucket.rb +179 -0
  45. data/modules/mu/config.rb +1 -1
  46. data/modules/mu/config/bucket.rb +69 -0
  47. data/modules/mu/config/bucket.yml +10 -0
  48. data/modules/mu/config/database.rb +1 -1
  49. data/modules/mu/config/endpoint.rb +71 -0
  50. data/modules/mu/config/function.rb +6 -0
  51. data/modules/mu/config/nosqldb.rb +49 -0
  52. data/modules/mu/config/nosqldb.yml +44 -0
  53. data/modules/mu/config/notifier.yml +2 -2
  54. data/modules/mu/config/vpc.rb +0 -1
  55. data/modules/mu/defaults/amazon_images.yaml +32 -30
  56. data/modules/mu/groomers/chef.rb +1 -1
  57. data/modules/mu/kittens.rb +2430 -1511
  58. data/modules/mu/master/ldap.rb +1 -1
  59. data/modules/tests/super_complex_bok.yml +7 -0
  60. data/modules/tests/super_simple_bok.yml +7 -0
  61. metadata +11 -2
@@ -0,0 +1,287 @@
1
+ # Copyright:: Copyright (c) 2019 eGlobalTech, Inc., all rights reserved
2
+ #
3
+ # Licensed under the BSD-3 license (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License in the root of the project or at
6
+ #
7
+ # http://egt-labs.com/mu/LICENSE.html
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ module MU
16
+ class Cloud
17
+ class AWS
18
+ # Support for AWS S3
19
+ class Bucket < MU::Cloud::Bucket
20
+ @deploy = nil
21
+ @config = nil
22
+
23
+ @@region_cache = {}
24
+ @@region_cache_semaphore = Mutex.new
25
+
26
+ attr_reader :mu_name
27
+ attr_reader :config
28
+ attr_reader :cloud_id
29
+
30
+ # @param mommacat [MU::MommaCat]: A {MU::Mommacat} object containing the deploy of which this resource is/will be a member.
31
+ # @param kitten_cfg [Hash]: The fully parsed and resolved {MU::Config} resource descriptor as defined in {MU::Config::BasketofKittens::logs}
32
+ def initialize(mommacat: nil, kitten_cfg: nil, mu_name: nil, cloud_id: nil)
33
+ @deploy = mommacat
34
+ @config = MU::Config.manxify(kitten_cfg)
35
+ @cloud_id ||= cloud_id
36
+ @mu_name ||= @deploy.getResourceName(@config["name"])
37
+ end
38
+
39
+ # Called automatically by {MU::Deploy#createResources}
40
+ def create
41
+ bucket_name = @deploy.getResourceName(@config["name"], max_length: 63).downcase
42
+ MU::Cloud::AWS.s3(credentials: @config['credentials'], region: @config['region']).create_bucket(
43
+ acl: @config['acl'],
44
+ bucket: bucket_name
45
+ )
46
+ @cloud_id = bucket_name
47
+
48
+ @@region_cache_semaphore.synchronize {
49
+ @@region_cache[@cloud_id] ||= @config['region']
50
+ }
51
+
52
+ tagBucket if !@config['scrub_mu_isms']
53
+ end
54
+
55
+ # Apply tags to this bucket object
56
+ def tagBucket
57
+ tagset = []
58
+
59
+ MU::MommaCat.listStandardTags.each_pair { |key, value|
60
+ tagset << { :key => key, :value => value }
61
+ }
62
+
63
+ if @config['tags']
64
+ @config['tags'].each { |tag|
65
+ tagset << { :key => tag['key'], :value => tag['value'] }
66
+ }
67
+ end
68
+
69
+ if @config['optional_tags']
70
+ MU::MommaCat.listOptionalTags.each { |key, value|
71
+ tagset << { :key => key, :value => value }
72
+ }
73
+ end
74
+
75
+ MU::Cloud::AWS.s3(credentials: @config['credentials'], region: @config['region']).put_bucket_tagging(
76
+ bucket: @cloud_id,
77
+ tagging: {
78
+ tag_set: tagset
79
+ }
80
+ )
81
+
82
+ end
83
+
84
+ # Called automatically by {MU::Deploy#createResources}
85
+ def groom
86
+ @@region_cache_semaphore.synchronize {
87
+ @@region_cache[@cloud_id] ||= @config['region']
88
+ }
89
+ tagBucket if !@config['scrub_mu_isms']
90
+
91
+ current = cloud_desc
92
+
93
+ if @config['web'] and current["website"].nil?
94
+ MU.log "Enabling web service on S3 bucket #{@cloud_id}", MU::NOTICE
95
+ MU::Cloud::AWS.s3(credentials: @config['credentials'], region: @config['region']).put_bucket_website(
96
+ bucket: @cloud_id,
97
+ website_configuration: {
98
+ error_document: {
99
+ key: @config['web_error_object']
100
+ },
101
+ index_document: {
102
+ suffix: @config['web_index_object']
103
+ }
104
+ }
105
+ )
106
+ elsif !@config['web'] and !current["website"].nil?
107
+ MU.log "Disabling web service on S3 bucket #{@cloud_id}", MU::NOTICE
108
+ MU::Cloud::AWS.s3(credentials: @config['credentials'], region: @config['region']).delete_bucket_website(
109
+ bucket: @cloud_id
110
+ )
111
+ end
112
+
113
+ if @config['versioning'] and current["versioning"].status != "Enabled"
114
+ MU.log "Enabling versioning on S3 bucket #{@cloud_id}", MU::NOTICE
115
+ MU::Cloud::AWS.s3(credentials: @config['credentials'], region: @config['region']).put_bucket_versioning(
116
+ bucket: @cloud_id,
117
+ versioning_configuration: {
118
+ mfa_delete: "Disabled",
119
+ status: "Enabled"
120
+ }
121
+ )
122
+ elsif !@config['versioning'] and current["versioning"].status == "Enabled"
123
+ MU.log "Suspending versioning on S3 bucket #{@cloud_id}", MU::NOTICE
124
+ MU::Cloud::AWS.s3(credentials: @config['credentials'], region: @config['region']).put_bucket_versioning(
125
+ bucket: @cloud_id,
126
+ versioning_configuration: {
127
+ mfa_delete: "Disabled",
128
+ status: "Suspended"
129
+ }
130
+ )
131
+ end
132
+ end
133
+
134
+ # Does this resource type exist as a global (cloud-wide) artifact, or
135
+ # is it localized to a region/zone?
136
+ # @return [Boolean]
137
+ def self.isGlobal?
138
+ false
139
+ end
140
+
141
+ # Remove all buckets associated with the currently loaded deployment.
142
+ # @param noop [Boolean]: If true, will only print what would be done
143
+ # @param ignoremaster [Boolean]: If true, will remove resources not flagged as originating from this Mu server
144
+ # @param region [String]: The cloud provider region
145
+ # @return [void]
146
+ def self.cleanup(noop: false, ignoremaster: false, region: MU.curRegion, credentials: nil, flags: {})
147
+
148
+ resp = MU::Cloud::AWS.s3(credentials: credentials, region: region).list_buckets
149
+ if resp and resp.buckets
150
+ resp.buckets.each { |bucket|
151
+ @@region_cache_semaphore.synchronize {
152
+ if @@region_cache[bucket.name]
153
+ next if @@region_cache[bucket.name] != region
154
+ else
155
+ location = MU::Cloud::AWS.s3(credentials: credentials, region: region).get_bucket_location(bucket: bucket.name).location_constraint
156
+
157
+ if location.nil? or location.empty?
158
+ @@region_cache[bucket.name] = region
159
+ else
160
+ @@region_cache[bucket.name] = location
161
+ end
162
+ end
163
+ }
164
+
165
+ if @@region_cache[bucket.name] != region
166
+ MU.log "#{bucket.name} is in #{@@region_cache[bucket.name]} but I'm checking from #{region}, skipping", MU::DEBUG
167
+ next
168
+ end
169
+
170
+ begin
171
+ tags = MU::Cloud::AWS.s3(credentials: credentials, region: region).get_bucket_tagging(bucket: bucket.name).tag_set
172
+ tags.each { |tag|
173
+ if tag.key == "MU-ID" and tag.value == MU.deploy_id
174
+ MU.log "Deleting S3 Bucket #{bucket.name}"
175
+ if !noop
176
+ MU::Cloud::AWS.s3(credentials: credentials, region: region).delete_bucket(bucket: bucket.name)
177
+ end
178
+ break
179
+ end
180
+ }
181
+ rescue Aws::S3::Errors::NoSuchTagSet, Aws::S3::Errors::PermanentRedirect
182
+ next
183
+ end
184
+ }
185
+ end
186
+ end
187
+
188
+ # Canonical Amazon Resource Number for this resource
189
+ # @return [String]
190
+ def arn
191
+ "arn:"+(MU::Cloud::AWS.isGovCloud?(@config["region"]) ? "aws-us-gov" : "aws")+":s3:::"+@cloud_id
192
+ end
193
+
194
+ # Return the metadata for this user cofiguration
195
+ # @return [Hash]
196
+ def notify
197
+ desc = MU::Cloud::AWS::Bucket.describe_bucket(@cloud_id, credentials: @config['credentials'], region: @config['region'])
198
+ MU.structToHash(desc)
199
+ end
200
+
201
+ # Locate an existing bucket.
202
+ # @param cloud_id [String]: The cloud provider's identifier for this resource.
203
+ # @param region [String]: The cloud provider region.
204
+ # @param flags [Hash]: Optional flags
205
+ # @return [OpenStruct]: The cloud provider's complete descriptions of matching bucket.
206
+ def self.find(cloud_id: nil, region: MU.curRegion, credentials: nil, flags: {})
207
+ found = {}
208
+ if cloud_id
209
+ found[cloud_id] = describe_bucket(cloud_id, minimal: true, credentials: credentials, region: region)
210
+ end
211
+ found
212
+ end
213
+
214
+ # Cloud-specific configuration properties.
215
+ # @param config [MU::Config]: The calling MU::Config object
216
+ # @return [Array<Array,Hash>]: List of required fields, and json-schema Hash of cloud-specific configuration parameters for this resource
217
+ def self.schema(config)
218
+ toplevel_required = []
219
+ schema = {
220
+ "acl" => {
221
+ "type" => "string",
222
+ "enum" => ["private", "public-read", "public-read-write", "authenticated-read"],
223
+ "default" => "private"
224
+ },
225
+ "storage_class" => {
226
+ "type" => "string",
227
+ "enum" => ["STANDARD", "REDUCED_REDUNDANCY", "STANDARD_IA", "ONEZONE_IA", "INTELLIGENT_TIERING", "GLACIER"],
228
+ "default" => "STANDARD"
229
+ }
230
+ }
231
+ [toplevel_required, schema]
232
+ end
233
+
234
+ # Cloud-specific pre-processing of {MU::Config::BasketofKittens::bucket}, bare and unvalidated.
235
+
236
+ # @param bucket [Hash]: The resource to process and validate
237
+ # @param configurator [MU::Config]: The overall deployment configurator of which this resource is a member
238
+ # @return [Boolean]: True if validation succeeded, False otherwise
239
+ def self.validateConfig(bucket, configurator)
240
+ ok = true
241
+
242
+ ok
243
+ end
244
+
245
+ private
246
+
247
+ # AWS doesn't really implement a useful describe_ method for S3 buckets;
248
+ # instead we run the million little individual API calls to construct
249
+ # an approximation for our uses
250
+ def self.describe_bucket(bucket, minimal: false, credentials: nil, region: nil)
251
+ @@region_cache = {}
252
+ @@region_cache_semaphore = Mutex.new
253
+ calls = if minimal
254
+ %w{encryption lifecycle lifecycle_configuration location logging policy replication tagging versioning website}
255
+ else
256
+ %w{accelerate_configuration acl cors encryption lifecycle lifecycle_configuration location logging notification notification_configuration policy policy_status replication request_payment tagging versioning website} # XXX analytics_configuration, inventory_configuration, metrics_configuration all require an id of some sort
257
+ end
258
+
259
+ desc = {}
260
+
261
+ calls.each { |method|
262
+ method_sym = ("get_bucket_"+method).to_sym
263
+ # "The horrors of this place claw at your mind"
264
+ begin
265
+ desc[method] = MU::Cloud::AWS.s3(credentials: credentials, region: region).method_missing(method_sym, {:bucket => bucket})
266
+ if method == "location"
267
+ @@region_cache_semaphore.synchronize {
268
+ if desc[method].location_constraint.nil? or desc[method].location_constraint.empty?
269
+ @@region_cache[bucket] = region
270
+ else
271
+ @@region_cache[bucket] = desc[method].location_constraint
272
+ end
273
+ }
274
+ end
275
+
276
+ rescue Aws::S3::Errors::NoSuchCORSConfiguration, Aws::S3::Errors::ServerSideEncryptionConfigurationNotFoundError, Aws::S3::Errors::NoSuchLifecycleConfiguration, Aws::S3::Errors::NoSuchBucketPolicy, Aws::S3::Errors::ReplicationConfigurationNotFoundError, Aws::S3::Errors::NoSuchTagSet, Aws::S3::Errors::NoSuchWebsiteConfiguration => e
277
+ desc[method] = nil
278
+ next
279
+ end
280
+ }
281
+ desc
282
+ end
283
+
284
+ end
285
+ end
286
+ end
287
+ end
@@ -364,18 +364,16 @@ module MU
364
364
  end
365
365
  elsif @config["creation_style"] == "new"
366
366
  MU.log "Creating pristine database instance #{@config['identifier']} (#{@config['name']}) in #{@config['region']}"
367
- puts @config['credentials']
368
- pp config
369
367
  resp = MU::Cloud::AWS.rds(region: @config['region'], credentials: @config['credentials']).create_db_instance(config)
370
368
  end
371
369
  rescue Aws::RDS::Errors::InvalidParameterValue => e
372
370
  if attempts < 5
373
- MU.log "Got #{e.inspect} creating #{@config['identifier']}, will retry a few times in case of transient errors.", MU::WARN
371
+ MU.log "Got #{e.inspect} creating #{@config['identifier']}, will retry a few times in case of transient errors.", MU::WARN, details: config
374
372
  attempts += 1
375
373
  sleep 10
376
374
  retry
377
375
  else
378
- raise MuError, "Exhausted retries trying to create database instance #{@config['identifier']}: e.inspect"
376
+ raise MuError, "Exhausted retries trying to create database instance #{@config['identifier']}: #{e.inspect}"
379
377
  end
380
378
  end
381
379
 
@@ -497,6 +495,18 @@ pp config
497
495
  }
498
496
  cluster_config_struct[:port] = @config["port"] if @config["port"]
499
497
 
498
+ if @config['cluster_mode']
499
+ cluster_config_struct[:engine_mode] = @config['cluster_mode']
500
+ if @config['cluster_mode'] == "serverless"
501
+ cluster_config_struct[:scaling_configuration] = {
502
+ :auto_pause => @config['serverless_scaling']['auto_pause'],
503
+ :min_capacity => @config['serverless_scaling']['min_capacity'],
504
+ :max_capacity => @config['serverless_scaling']['max_capacity'],
505
+ :seconds_until_auto_pause => @config['serverless_scaling']['seconds_until_auto_pause']
506
+ }
507
+ end
508
+ end
509
+
500
510
  if %w{existing_snapshot new_snapshot}.include?(@config["creation_style"])
501
511
  cluster_config_struct[:snapshot_identifier] = @config["snapshot_id"]
502
512
  cluster_config_struct[:engine] = @config["engine"]
@@ -537,12 +547,13 @@ pp config
537
547
  end
538
548
  rescue Aws::RDS::Errors::InvalidParameterValue => e
539
549
  if attempts < 5
540
- MU.log "Got #{e.inspect} while creating database cluster #{@config['identifier']}, will retry a few times in case of transient errors.", MU::WARN
550
+ MU.log "Got #{e.inspect} while creating database cluster #{@config['identifier']}, will retry a few times in case of transient errors.", MU::WARN, details: cluster_config_struct
541
551
  attempts += 1
542
552
  sleep 10
543
553
  retry
544
554
  else
545
- raise MuError, "Exhausted retries trying to create database cluster #{@config['identifier']}", MU::ERR, details: e.inspect
555
+ MU.log "Exhausted retries trying to create database cluster #{@config['identifier']}", MU::ERR, details: e.inspect
556
+ raise MuError, "Exhausted retries trying to create database cluster #{@config['identifier']}"
546
557
  end
547
558
  end
548
559
 
@@ -1414,6 +1425,45 @@ pp config
1414
1425
  schema = {
1415
1426
  "db_parameter_group_parameters" => rds_parameters_primitive,
1416
1427
  "cluster_parameter_group_parameters" => rds_parameters_primitive,
1428
+ "cluster_mode" => {
1429
+ "type" => "string",
1430
+ "description" => "The DB engine mode of the DB cluster",
1431
+ "enum" => ["provisioned", "serverless", "parallelquery", "global"],
1432
+ "default" => "provisioned"
1433
+ },
1434
+ "serverless_scaling" => {
1435
+ "type" => "object",
1436
+ "descriptions" => "Scaling configuration for a +serverless+ Aurora cluster",
1437
+ "default" => {
1438
+ "auto_pause" => false,
1439
+ "min_capacity" => 2,
1440
+ "max_capacity" => 2
1441
+ },
1442
+ "properties" => {
1443
+ "auto_pause" => {
1444
+ "type" => "boolean",
1445
+ "description" => "A value that specifies whether to allow or disallow automatic pause for an Aurora DB cluster in serverless DB engine mode",
1446
+ "default" => false
1447
+ },
1448
+ "min_capacity" => {
1449
+ "type" => "integer",
1450
+ "description" => "The minimum capacity for an Aurora DB cluster in serverless DB engine mode.",
1451
+ "default" => 2,
1452
+ "enum" => [2, 4, 8, 16, 32, 64, 128, 256]
1453
+ },
1454
+ "max_capacity" => {
1455
+ "type" => "integer",
1456
+ "description" => "The maximum capacity for an Aurora DB cluster in serverless DB engine mode.",
1457
+ "default" => 2,
1458
+ "enum" => [2, 4, 8, 16, 32, 64, 128, 256]
1459
+ },
1460
+ "seconds_until_auto_pause" => {
1461
+ "type" => "integer",
1462
+ "description" => "A DB cluster can be paused only when it's idle (it has no connections). If a DB cluster is paused for more than seven days, the DB cluster might be backed up with a snapshot. In this case, the DB cluster is restored when there is a request to connect to it.",
1463
+ "default" => 86400
1464
+ }
1465
+ }
1466
+ },
1417
1467
  "license_model" => {
1418
1468
  "type" => "string",
1419
1469
  "enum" => ["license-included", "bring-your-own-license", "general-public-license", "postgresql-license"]
@@ -1452,7 +1502,11 @@ pp config
1452
1502
  if db['create_cluster'] or db['engine'] == "aurora" or db["member_of_cluster"]
1453
1503
  case db['engine']
1454
1504
  when "mysql", "aurora", "aurora-mysql"
1455
- db["engine"] = "aurora-mysql"
1505
+ if db["engine_version"] == "5.6" or db["cluster_mode"] == "serverless"
1506
+ db["engine"] = "aurora"
1507
+ else
1508
+ db["engine"] = "aurora-mysql"
1509
+ end
1456
1510
  when "postgres", "postgresql", "postgresql-mysql"
1457
1511
  db["engine"] = "aurora-postgresql"
1458
1512
  else
@@ -1671,14 +1725,14 @@ pp config
1671
1725
  end
1672
1726
 
1673
1727
  # Cleanup the database vault
1674
- grommer =
1728
+ groomer =
1675
1729
  if database_obj
1676
1730
  database_obj.config.has_key?("groomer") ? database_obj.config["groomer"] : MU::Config.defaultGroomer
1677
1731
  else
1678
1732
  MU::Config.defaultGroomer
1679
1733
  end
1680
1734
 
1681
- groomclass = MU::Groomer.loadGroomer(grommer)
1735
+ groomclass = MU::Groomer.loadGroomer(groomer)
1682
1736
  groomclass.deleteSecret(vault: db_id.upcase) if !noop
1683
1737
  MU.log "#{db_id} has been terminated"
1684
1738
  end
@@ -1761,14 +1815,14 @@ pp config
1761
1815
  end
1762
1816
 
1763
1817
  # Cleanup the cluster vault
1764
- grommer =
1818
+ groomer =
1765
1819
  if cluster_obj
1766
1820
  cluster_obj.config.has_key?("groomer") ? cluster_obj.config["groomer"] : MU::Config.defaultGroomer
1767
1821
  else
1768
1822
  MU::Config.defaultGroomer
1769
1823
  end
1770
1824
 
1771
- groomclass = MU::Groomer.loadGroomer(grommer)
1825
+ groomclass = MU::Groomer.loadGroomer(groomer)
1772
1826
  groomclass.deleteSecret(vault: cluster_id.upcase) if !noop
1773
1827
 
1774
1828
  MU.log "#{cluster_id} has been terminated"
@@ -0,0 +1,592 @@
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
+ @deploy = nil
7
+ @config = nil
8
+ attr_reader :mu_name
9
+ attr_reader :config
10
+ attr_reader :cloud_id
11
+
12
+ @cloudformation_data = {}
13
+ attr_reader :cloudformation_data
14
+
15
+ # @param mommacat [MU::MommaCat]: A {MU::Mommacat} object containing the deploy of which this resource is/will be a member.
16
+ # @param kitten_cfg [Hash]: The fully parsed and resolved {MU::Config} resource descriptor as defined in {MU::Config::BasketofKittens::endpoints}
17
+ def initialize(mommacat: nil, kitten_cfg: nil, mu_name: nil, cloud_id: nil)
18
+ @deploy = mommacat
19
+ @config = MU::Config.manxify(kitten_cfg)
20
+ @cloud_id ||= cloud_id
21
+ @mu_name ||= @deploy.getResourceName(@config["name"])
22
+ end
23
+
24
+ # Called automatically by {MU::Deploy#createResources}
25
+ def create
26
+ resp = MU::Cloud::AWS.apig(region: @config['region'], credentials: @config['credentials']).create_rest_api(
27
+ name: @mu_name,
28
+ description: @deploy.deploy_id,
29
+ endpoint_configuration: {
30
+ types: ["REGIONAL"] # XXX expose in BoK ["REGIONAL", "EDGE", "PRIVATE"]
31
+ }
32
+ )
33
+ @cloud_id = resp.id
34
+ generate_methods
35
+
36
+
37
+ end
38
+
39
+ # Create/update all of the methods declared for this endpoint
40
+ def generate_methods
41
+ resp = MU::Cloud::AWS.apig(region: @config['region'], credentials: @config['credentials']).get_resources(
42
+ rest_api_id: @cloud_id,
43
+ )
44
+ root_resource = resp.items.first.id
45
+
46
+ # TODO guard this crap so we don't touch it if there are no changes
47
+ @config['methods'].each { |m|
48
+ method_arn = "arn:#{MU::Cloud::AWS.isGovCloud?(@config["region"]) ? "aws-us-gov" : "aws"}:execute-api:#{@config["region"]}:#{MU::Cloud::AWS.credToAcct(@config['credentials'])}:#{@cloud_id}/*/#{m['type']}/#{m['path']}"
49
+
50
+ resp = MU::Cloud::AWS.apig(region: @config['region'], credentials: @config['credentials']).get_resources(
51
+ rest_api_id: @cloud_id
52
+ )
53
+ ext_resource = nil
54
+ resp.items.each { |resource|
55
+ if resource.path_part == m['path']
56
+ ext_resource = resource.id
57
+ end
58
+ }
59
+
60
+ resp = if ext_resource
61
+ MU::Cloud::AWS.apig(region: @config['region'], credentials: @config['credentials']).get_resource(
62
+ rest_api_id: @cloud_id,
63
+ resource_id: ext_resource,
64
+ )
65
+ # MU::Cloud::AWS.apig(region: @config['region'], credentials: @config['credentials']).update_resource(
66
+ # rest_api_id: @cloud_id,
67
+ # resource_id: ext_resource,
68
+ # patch_operations: [
69
+ # {
70
+ # op: "replace",
71
+ # path: "XXX ??",
72
+ # value: m["path"]
73
+ # }
74
+ # ]
75
+ # )
76
+ else
77
+ MU::Cloud::AWS.apig(region: @config['region'], credentials: @config['credentials']).create_resource(
78
+ rest_api_id: @cloud_id,
79
+ parent_id: root_resource,
80
+ path_part: m['path']
81
+ )
82
+ end
83
+ parent_id = resp.id
84
+
85
+ resp = begin
86
+ MU::Cloud::AWS.apig(region: @config['region'], credentials: @config['credentials']).get_method(
87
+ rest_api_id: @cloud_id,
88
+ resource_id: parent_id,
89
+ http_method: m['type']
90
+ )
91
+ rescue Aws::APIGateway::Errors::NotFoundException
92
+ resp = MU::Cloud::AWS.apig(region: @config['region'], credentials: @config['credentials']).put_method(
93
+ rest_api_id: @cloud_id,
94
+ resource_id: parent_id,
95
+ authorization_type: m['auth'],
96
+ http_method: m['type']
97
+ )
98
+ end
99
+
100
+ # XXX effectively a placeholder default
101
+ begin
102
+ m['responses'].each { |r|
103
+ params = {
104
+ :rest_api_id => @cloud_id,
105
+ :resource_id => parent_id,
106
+ :http_method => m['type'],
107
+ :status_code => r['code'].to_s
108
+ }
109
+ if r['headers']
110
+ params[:response_parameters] = r['headers'].map { |h|
111
+ ["method.response.header."+h['header'], h['required']]
112
+ }.to_h
113
+ end
114
+
115
+ if r['body']
116
+ # 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
117
+ params[:response_models] = r['body'].map { |b| [b['content_type'], b['is_error'] ? "Error" : "Empty"] }.to_h
118
+ end
119
+
120
+ MU::Cloud::AWS.apig(region: @config['region'], credentials: @config['credentials']).put_method_response(params)
121
+ }
122
+ rescue Aws::APIGateway::Errors::ConflictException
123
+ # fine to ignore
124
+ end
125
+
126
+ if m['integrate_with']
127
+ role_arn = if m['iam_role']
128
+ if m['iam_role'].match(/^arn:/)
129
+ m['iam_role']
130
+ else
131
+ sib_role = @deploy.findLitterMate(name: m['iam_role'], type: "roles")
132
+ sib_role.cloudobj.arn
133
+ # XXX make this more like get_role_arn in Function, or just use Role.find?
134
+ end
135
+ end
136
+
137
+ function_obj = nil
138
+
139
+ uri, type = if m['integrate_with']['type'] == "aws_generic"
140
+ svc, action = m['integrate_with']['aws_generic_action'].split(/:/)
141
+ ["arn:aws:apigateway:"+@config['region']+":#{svc}:action/#{action}", "AWS"]
142
+ elsif m['integrate_with']['type'] == "function"
143
+ function_obj = @deploy.findLitterMate(name: m['integrate_with']['name'], type: "functions").cloudobj
144
+ ["arn:aws:apigateway:"+@config['region']+":lambda:path/2015-03-31/functions/"+function_obj.arn+"/invocations", "AWS"]
145
+ elsif m['integrate_with']['type'] == "mock"
146
+ [nil, "MOCK"]
147
+ end
148
+
149
+ params = {
150
+ :rest_api_id => @cloud_id,
151
+ :resource_id => parent_id,
152
+ :type => type, # XXX Lambda and Firehose can do AWS_PROXY
153
+ :content_handling => "CONVERT_TO_TEXT", # XXX expose in BoK
154
+ :http_method => m['type']
155
+ # credentials: role_arn
156
+ }
157
+ params[:uri] = uri if uri
158
+
159
+ if m['integrate_with']['type'] != "mock"
160
+ params[:integration_http_method] = m['integrate_with']['backend_http_method']
161
+ else
162
+ params[:integration_http_method] = nil
163
+ end
164
+
165
+ if m['integrate_with']['passthrough_behavior']
166
+ params[:passthrough_behavior] = m['integrate_with']['passthrough_behavior']
167
+ end
168
+ if m['integrate_with']['request_templates']
169
+ params[:request_templates] = {}
170
+ m['integrate_with']['request_templates'].each { |rt|
171
+ params[:request_templates][rt['content_type']] = rt['template']
172
+ }
173
+ end
174
+
175
+ resp = MU::Cloud::AWS.apig(region: @config['region'], credentials: @config['credentials']).put_integration(params)
176
+
177
+ if m['integrate_with']['type'] == "function"
178
+ function_obj.addTrigger(method_arn, "apigateway", @config['name'])
179
+ end
180
+
181
+ m['responses'].each { |r|
182
+ params = {
183
+ :rest_api_id => @cloud_id,
184
+ :resource_id => parent_id,
185
+ :http_method => m['type'],
186
+ :status_code => r['code'].to_s,
187
+ :selection_pattern => ""
188
+ }
189
+ if r['headers']
190
+ params[:response_parameters] = r['headers'].map { |h|
191
+ ["method.response.header."+h['header'], "'"+h['value']+"'"]
192
+ }.to_h
193
+ end
194
+
195
+ MU::Cloud::AWS.apig(region: @config['region'], credentials: @config['credentials']).put_integration_response(params)
196
+
197
+ }
198
+
199
+ end
200
+
201
+ }
202
+ end
203
+
204
+ # Called automatically by {MU::Deploy#createResources}
205
+ def groom
206
+ generate_methods
207
+
208
+ MU.log "Deploying API Gateway #{@config['name']} to #{@config['deploy_to']}"
209
+ resp = MU::Cloud::AWS.apig(region: @config['region'], credentials: @config['credentials']).create_deployment(
210
+ rest_api_id: @cloud_id,
211
+ stage_name: @config['deploy_to']
212
+ # cache_cluster_enabled: false,
213
+ # cache_cluster_size: 0.5,
214
+ )
215
+ deployment_id = resp.id
216
+ # this automatically creates a stage with the same name, so we don't
217
+ # have to deal with that
218
+
219
+ my_url = "https://"+@cloud_id+".execute-api."+@config['region']+".amazonaws.com/"+@config['deploy_to']
220
+ MU.log "API Endpoint #{@config['name']}: "+my_url, MU::SUMMARY
221
+
222
+ # resp = MU::Cloud::AWS.apig(region: @config['region'], credentials: @config['credentials']).create_authorizer(
223
+ # rest_api_id: @cloud_id,
224
+ # )
225
+
226
+ # resp = MU::Cloud::AWS.apig(region: @config['region'], credentials: @config['credentials']).create_vpc_link(
227
+ # )
228
+
229
+ end
230
+
231
+ # @return [Struct]
232
+ def cloud_desc
233
+ MU::Cloud::AWS.apig(region: @config['region'], credentials: @config['credentials']).get_rest_api(
234
+ rest_api_id: @cloud_id
235
+ )
236
+ end
237
+
238
+ # Return the metadata for this API
239
+ # @return [Hash]
240
+ def notify
241
+ deploy_struct = MU.structToHash(cloud_desc)
242
+ # XXX stages and whatnot
243
+ return deploy_struct
244
+ end
245
+
246
+ # Remove all APIs associated with the currently loaded deployment.
247
+ # @param noop [Boolean]: If true, will only print what would be done
248
+ # @param ignoremaster [Boolean]: If true, will remove resources not flagged as originating from this Mu server
249
+ # @param region [String]: The cloud provider region
250
+ # @return [void]
251
+ def self.cleanup(noop: false, ignoremaster: false, region: MU.curRegion, credentials: nil, flags: {})
252
+ resp = MU::Cloud::AWS.apig(region: region, credentials: credentials).get_rest_apis
253
+ if resp and resp.items
254
+ resp.items.each { |api|
255
+ # The stupid things don't have tags
256
+ if api.description == MU.deploy_id
257
+ MU.log "Deleting API Gateway #{api.name} (#{api.id})"
258
+ if !noop
259
+ MU::Cloud::AWS.apig(region: region, credentials: credentials).delete_rest_api(
260
+ rest_api_id: api.id
261
+ )
262
+ end
263
+ end
264
+ }
265
+ end
266
+ end
267
+
268
+ # Locate an existing API.
269
+ # @param cloud_id [String]: The cloud provider's identifier for this resource.
270
+ # @param region [String]: The cloud provider region.
271
+ # @param flags [Hash]: Optional flags
272
+ # @return [OpenStruct]: The cloud provider's complete descriptions of matching API.
273
+ def self.find(cloud_id: nil, region: MU.curRegion, credentials: nil, flags: {})
274
+ if cloud_id
275
+ return MU::Cloud::AWS.apig(region: region, credentials: credentials).get_rest_api(
276
+ rest_api_id: cloud_id
277
+ )
278
+ end
279
+ # resp = MU::Cloud::AWS.apig(region: region, credentials: credentials).get_rest_apis
280
+ # if resp and resp.items
281
+ # resp.items.each { |api|
282
+ # }
283
+ # end
284
+ nil
285
+ end
286
+
287
+ # Cloud-specific configuration properties.
288
+ # @param config [MU::Config]: The calling MU::Config object
289
+ # @return [Array<Array,Hash>]: List of required fields, and json-schema Hash of cloud-specific configuration parameters for this resource
290
+ def self.schema(config)
291
+ toplevel_required = []
292
+ schema = {
293
+ "deploy_to" => {
294
+ "type" => "string",
295
+ "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."
296
+ },
297
+ "methods" => {
298
+ "items" => {
299
+ "type" => "object",
300
+ "description" => "Other cloud resources to integrate as a back end to this API Gateway",
301
+ "required" => ["integrate_with"],
302
+ "properties" => {
303
+ "integrate_with" => {
304
+ "type" => "object",
305
+ "description" => "Specify what application backend to invoke under this path/method combination",
306
+ "properties" => {
307
+ "proxy" => {
308
+ "type" => "boolean",
309
+ "default" => false,
310
+ "description" => "For HTTP or AWS integrations, specify whether the target is a proxy (((docs unclear, is that actually what this means?)))" # XXX is that actually what this means?
311
+ },
312
+ "backend_http_method" => {
313
+ "type" => "string",
314
+ "description" => "The HTTP method to use when contacting our integrated backend. If not specified, this will be set to match our front end.",
315
+ "enum" => ["GET", "POST", "PUT", "HEAD", "DELETE", "CONNECT", "OPTIONS", "TRACE"],
316
+ },
317
+ "url" => {
318
+ "type" => "string",
319
+ "description" => "For HTTP or HTTP_PROXY integrations, this should be a fully-qualified URL"
320
+ },
321
+ "responses"=> {
322
+ "type" => "array",
323
+ "items" => {
324
+ "type" => "object",
325
+ "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.",
326
+ "properties" => {
327
+ "code" => {
328
+ "type" => "integer",
329
+ "description" => "The HTTP status code to return",
330
+ "default" => 200
331
+ },
332
+ "headers" => {
333
+ "type" => "array",
334
+ "items" => {
335
+ "description" => "One or more headers, used by the API Gateway integration response and filtered through the method response before returning to the client",
336
+ "type" => "object",
337
+ "properties" => {
338
+ "header" => {
339
+ "type" => "string",
340
+ "description" => "The name of a header to return, such as +Access-Control-Allow-Methods+"
341
+ },
342
+ "value" => {
343
+ "type" => "string",
344
+ "description" => "The string to map to this header (ex +GET,OPTIONS+)"
345
+ },
346
+ "required" => {
347
+ "type" => "boolean",
348
+ "description" => "Indicate whether this header is required in order to return a response",
349
+ "default" => true
350
+ }
351
+ }
352
+ }
353
+ },
354
+ "body" => {
355
+ "type" => "array",
356
+ "items" => {
357
+ "type" => "object",
358
+ "description" => "Model for the body of our backend integration's response",
359
+ "properties" => {
360
+ "content_type" => {
361
+ "type" => "string",
362
+ "description" => "An HTTP content type to match to a response, such as +application/json+."
363
+ },
364
+ "is_error" => {
365
+ "type" => "boolean",
366
+ "description" => "Whether this response should be considered an error",
367
+ "default" => false
368
+ }
369
+ }
370
+ }
371
+ }
372
+ }
373
+ }
374
+ },
375
+ "arn" => {
376
+ "type" => "string",
377
+ "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`"
378
+ },
379
+ "name" => {
380
+ "type" => "string",
381
+ "description" => "A Mu resource name, for integrations with a sibling resource (e.g. a Function)"
382
+ },
383
+ "cors" => {
384
+ "type" => "boolean",
385
+ "description" => "When enabled, this will create an +OPTIONS+ method under this path with request and response header mappings that implement Cross-Origin Resource Sharing",
386
+ "default" => true
387
+ },
388
+ "type" => {
389
+ "type" => "string",
390
+ "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.",
391
+ "enum" => ["aws_generic"].concat(MU::Cloud.resource_types.values.map { |t| t[:cfg_name] }.sort)
392
+ },
393
+ "aws_generic_action" => {
394
+ "type" => "string",
395
+ "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."
396
+ },
397
+ "deploy_id" => {
398
+ "type" => "string",
399
+ "description" => "A Mu deploy id (e.g. DEMO-DEV-2014111400-NG), for integrations with a sibling resource (e.g. a Function)"
400
+ },
401
+ "iam_role" => {
402
+ "type" => "string",
403
+ "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."
404
+ },
405
+ "passthrough_behavior" => {
406
+ "type" => "string",
407
+ "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+.",
408
+ "enum" => ["WHEN_NO_MATCH", "WHEN_NO_TEMPLATES", "NEVER"],
409
+ "default" => "WHEN_NO_MATCH"
410
+ },
411
+ "request_templates" => {
412
+ "type" => "array",
413
+ "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.",
414
+ "items" => {
415
+ "type" => "object",
416
+ "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.",
417
+ "require" => ["content_type", "template"],
418
+ "properties" => {
419
+ "content_type" => {
420
+ "type" => "string",
421
+ "description" => "An HTTP content type to match with a template, such as +application/json+."
422
+ },
423
+ "template" => {
424
+ "type" => "string",
425
+ "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>'
426
+ }
427
+ }
428
+ }
429
+ }
430
+ }
431
+ },
432
+ "auth" => {
433
+ "type" => "string",
434
+ "enum" => ["NONE", "CUSTOM", "AWS_IAM", "COGNITO_USER_POOLS"],
435
+ "default" => "NONE"
436
+ }
437
+ }
438
+ }
439
+ }
440
+ }
441
+ [toplevel_required, schema]
442
+ end
443
+
444
+ # Does this resource type exist as a global (cloud-wide) artifact, or
445
+ # is it localized to a region/zone?
446
+ # @return [Boolean]
447
+ def self.isGlobal?
448
+ false
449
+ end
450
+
451
+ # Canonical Amazon Resource Number for this resource
452
+ # @return [String]
453
+ def arn
454
+ "arn:#{MU::Cloud::AWS.isGovCloud?(@config["region"]) ? "aws-us-gov" : "aws"}:execute-api:#{@config["region"]}:#{MU::Cloud::AWS.credToAcct(@config['credentials'])}:#{@cloud_id}"
455
+ end
456
+
457
+
458
+ # Cloud-specific pre-processing of {MU::Config::BasketofKittens::endpoints}, bare and unvalidated.
459
+ # @param endpoint [Hash]: The resource to process and validate
460
+ # @param configurator [MU::Config]: The overall deployment configurator of which this resource is a member
461
+ # @return [Boolean]: True if validation succeeded, False otherwise
462
+ def self.validateConfig(endpoint, configurator)
463
+ ok = true
464
+
465
+ append = []
466
+ endpoint['deploy_to'] ||= MU.environment || $environment || "dev"
467
+ endpoint['methods'].each { |m|
468
+ if m['integrate_with'] and m['integrate_with']['name']
469
+ if m['integrate_with']['type'] != "aws_generic"
470
+ endpoint['dependencies'] ||= []
471
+ endpoint['dependencies'] << {
472
+ "type" => m['integrate_with']['type'],
473
+ "name" => m['integrate_with']['name']
474
+ }
475
+ end
476
+
477
+ m['integrate_with']['backend_http_method'] ||= m['type']
478
+
479
+ m['responses'] ||= [
480
+ "code" => 200
481
+ ]
482
+
483
+ if m['cors']
484
+ m['responses'].each { |r|
485
+ r['headers'] ||= []
486
+ r['headers'] << {
487
+ "header" => "Access-Control-Allow-Origin",
488
+ "value" => "*",
489
+ "required" => true
490
+ }
491
+ r['headers'].uniq!
492
+ }
493
+
494
+ append << cors_option_integrations(m['path'])
495
+ end
496
+
497
+ if !m['iam_role']
498
+ m['uri'] ||= "*" if m['integrate_with']['type'] == "aws_generic"
499
+
500
+ roledesc = {
501
+ "name" => endpoint['name']+"-"+m['integrate_with']['name'],
502
+ "credentials" => endpoint['credentials'],
503
+ "can_assume" => [
504
+ {
505
+ "entity_id" => "apigateway.amazonaws.com",
506
+ "entity_type" => "service"
507
+ }
508
+ ],
509
+ }
510
+ if m['integrate_with']['type'] == "aws_generic"
511
+ roledesc["policies"] = [
512
+ {
513
+ "name" => m['integrate_with']['aws_generic_action'].gsub(/[^a-z]/i, ""),
514
+ "permissions" => [m['integrate_with']['aws_generic_action']],
515
+ "targets" => [{ "identifier" => m['uri'] }]
516
+ }
517
+ ]
518
+ elsif m['integrate_with']['type'] == "function"
519
+ roledesc["import"] = ["AWSLambdaBasicExecutionRole"]
520
+ end
521
+ configurator.insertKitten(roledesc, "roles")
522
+
523
+ endpoint['dependencies'] ||= []
524
+ m['iam_role'] = endpoint['name']+"-"+m['integrate_with']['name']
525
+
526
+ endpoint['dependencies'] << {
527
+ "type" => "role",
528
+ "name" => endpoint['name']+"-"+m['integrate_with']['name']
529
+ }
530
+ end
531
+ end
532
+ }
533
+ endpoint['methods'].concat(append.uniq) if endpoint['methods']
534
+ # if something_bad
535
+ # ok = false
536
+ # end
537
+
538
+ ok
539
+ end
540
+
541
+ private
542
+
543
+ def self.cors_option_integrations(path)
544
+ {
545
+ "type" => "OPTIONS",
546
+ "path" => path,
547
+ "auth" => "NONE",
548
+ "responses" => [
549
+ {
550
+ "code" => 200,
551
+ "headers" => [
552
+ {
553
+ "header" => "Access-Control-Allow-Headers",
554
+ "value" => "Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token",
555
+ "required" => true
556
+ },
557
+ {
558
+ "header" => "Access-Control-Allow-Methods",
559
+ "value" => "GET,OPTIONS",
560
+ "required" => true
561
+ },
562
+ {
563
+ "header" => "Access-Control-Allow-Origin",
564
+ "value" => "*",
565
+ "required" => true
566
+ }
567
+ ],
568
+ "body" => [
569
+ {
570
+ "content_type" => "application/json"
571
+ }
572
+ ]
573
+ }
574
+ ],
575
+ "integrate_with" => {
576
+ "type" => "mock",
577
+ "passthrough_behavior" => "WHEN_NO_MATCH",
578
+ "backend_http_method" => "OPTIONS",
579
+ "request_templates" => [
580
+ {
581
+ "content_type" => "application/json",
582
+ "template" => '{"statusCode": 200}'
583
+ }
584
+ ]
585
+ }
586
+ }
587
+ end
588
+
589
+ end
590
+ end
591
+ end
592
+ end