cloud-mu 3.1.3 → 3.1.4

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 (114) hide show
  1. checksums.yaml +4 -4
  2. data/Dockerfile +10 -2
  3. data/bin/mu-adopt +5 -1
  4. data/bin/mu-load-config.rb +2 -3
  5. data/bin/mu-run-tests +112 -27
  6. data/cloud-mu.gemspec +20 -20
  7. data/cookbooks/mu-tools/libraries/helper.rb +2 -1
  8. data/cookbooks/mu-tools/libraries/monkey.rb +35 -0
  9. data/cookbooks/mu-tools/recipes/google_api.rb +2 -2
  10. data/cookbooks/mu-tools/resources/disk.rb +1 -1
  11. data/extras/image-generators/Google/centos6.yaml +1 -0
  12. data/extras/image-generators/Google/centos7.yaml +1 -1
  13. data/modules/mommacat.ru +5 -15
  14. data/modules/mu.rb +10 -14
  15. data/modules/mu/adoption.rb +20 -14
  16. data/modules/mu/cleanup.rb +13 -9
  17. data/modules/mu/cloud.rb +26 -26
  18. data/modules/mu/clouds/aws.rb +100 -59
  19. data/modules/mu/clouds/aws/alarm.rb +4 -2
  20. data/modules/mu/clouds/aws/bucket.rb +25 -21
  21. data/modules/mu/clouds/aws/cache_cluster.rb +25 -23
  22. data/modules/mu/clouds/aws/collection.rb +21 -20
  23. data/modules/mu/clouds/aws/container_cluster.rb +47 -26
  24. data/modules/mu/clouds/aws/database.rb +57 -68
  25. data/modules/mu/clouds/aws/dnszone.rb +14 -14
  26. data/modules/mu/clouds/aws/endpoint.rb +20 -16
  27. data/modules/mu/clouds/aws/firewall_rule.rb +19 -16
  28. data/modules/mu/clouds/aws/folder.rb +7 -7
  29. data/modules/mu/clouds/aws/function.rb +15 -12
  30. data/modules/mu/clouds/aws/group.rb +14 -10
  31. data/modules/mu/clouds/aws/habitat.rb +16 -13
  32. data/modules/mu/clouds/aws/loadbalancer.rb +16 -15
  33. data/modules/mu/clouds/aws/log.rb +13 -10
  34. data/modules/mu/clouds/aws/msg_queue.rb +15 -8
  35. data/modules/mu/clouds/aws/nosqldb.rb +18 -11
  36. data/modules/mu/clouds/aws/notifier.rb +11 -6
  37. data/modules/mu/clouds/aws/role.rb +87 -70
  38. data/modules/mu/clouds/aws/search_domain.rb +30 -19
  39. data/modules/mu/clouds/aws/server.rb +102 -72
  40. data/modules/mu/clouds/aws/server_pool.rb +47 -28
  41. data/modules/mu/clouds/aws/storage_pool.rb +5 -6
  42. data/modules/mu/clouds/aws/user.rb +13 -10
  43. data/modules/mu/clouds/aws/vpc.rb +135 -121
  44. data/modules/mu/clouds/azure.rb +16 -9
  45. data/modules/mu/clouds/azure/container_cluster.rb +2 -3
  46. data/modules/mu/clouds/azure/firewall_rule.rb +10 -10
  47. data/modules/mu/clouds/azure/habitat.rb +8 -6
  48. data/modules/mu/clouds/azure/loadbalancer.rb +5 -5
  49. data/modules/mu/clouds/azure/role.rb +8 -10
  50. data/modules/mu/clouds/azure/server.rb +65 -25
  51. data/modules/mu/clouds/azure/user.rb +5 -7
  52. data/modules/mu/clouds/azure/vpc.rb +12 -15
  53. data/modules/mu/clouds/cloudformation.rb +8 -7
  54. data/modules/mu/clouds/cloudformation/vpc.rb +2 -4
  55. data/modules/mu/clouds/google.rb +39 -24
  56. data/modules/mu/clouds/google/bucket.rb +9 -11
  57. data/modules/mu/clouds/google/container_cluster.rb +27 -42
  58. data/modules/mu/clouds/google/database.rb +6 -9
  59. data/modules/mu/clouds/google/firewall_rule.rb +11 -10
  60. data/modules/mu/clouds/google/folder.rb +16 -9
  61. data/modules/mu/clouds/google/function.rb +127 -161
  62. data/modules/mu/clouds/google/group.rb +21 -18
  63. data/modules/mu/clouds/google/habitat.rb +18 -15
  64. data/modules/mu/clouds/google/loadbalancer.rb +14 -16
  65. data/modules/mu/clouds/google/role.rb +48 -31
  66. data/modules/mu/clouds/google/server.rb +105 -105
  67. data/modules/mu/clouds/google/server_pool.rb +12 -31
  68. data/modules/mu/clouds/google/user.rb +67 -13
  69. data/modules/mu/clouds/google/vpc.rb +58 -65
  70. data/modules/mu/config.rb +89 -1738
  71. data/modules/mu/config/bucket.rb +3 -3
  72. data/modules/mu/config/collection.rb +3 -3
  73. data/modules/mu/config/container_cluster.rb +2 -2
  74. data/modules/mu/config/dnszone.rb +5 -5
  75. data/modules/mu/config/doc_helpers.rb +517 -0
  76. data/modules/mu/config/endpoint.rb +3 -3
  77. data/modules/mu/config/firewall_rule.rb +118 -3
  78. data/modules/mu/config/folder.rb +3 -3
  79. data/modules/mu/config/function.rb +2 -2
  80. data/modules/mu/config/group.rb +3 -3
  81. data/modules/mu/config/habitat.rb +3 -3
  82. data/modules/mu/config/loadbalancer.rb +3 -3
  83. data/modules/mu/config/log.rb +3 -3
  84. data/modules/mu/config/msg_queue.rb +3 -3
  85. data/modules/mu/config/nosqldb.rb +3 -3
  86. data/modules/mu/config/notifier.rb +2 -2
  87. data/modules/mu/config/ref.rb +333 -0
  88. data/modules/mu/config/role.rb +3 -3
  89. data/modules/mu/config/schema_helpers.rb +508 -0
  90. data/modules/mu/config/search_domain.rb +3 -3
  91. data/modules/mu/config/server.rb +86 -58
  92. data/modules/mu/config/server_pool.rb +2 -2
  93. data/modules/mu/config/tail.rb +189 -0
  94. data/modules/mu/config/user.rb +3 -3
  95. data/modules/mu/config/vpc.rb +44 -4
  96. data/modules/mu/defaults/Google.yaml +2 -2
  97. data/modules/mu/deploy.rb +13 -10
  98. data/modules/mu/groomer.rb +1 -1
  99. data/modules/mu/groomers/ansible.rb +69 -24
  100. data/modules/mu/groomers/chef.rb +52 -44
  101. data/modules/mu/logger.rb +17 -14
  102. data/modules/mu/master.rb +317 -2
  103. data/modules/mu/master/chef.rb +3 -4
  104. data/modules/mu/master/ldap.rb +3 -3
  105. data/modules/mu/master/ssl.rb +12 -2
  106. data/modules/mu/mommacat.rb +85 -1766
  107. data/modules/mu/mommacat/daemon.rb +394 -0
  108. data/modules/mu/mommacat/naming.rb +366 -0
  109. data/modules/mu/mommacat/storage.rb +689 -0
  110. data/modules/tests/bucket.yml +4 -0
  111. data/modules/tests/{win2k12.yaml → needwork/win2k12.yaml} +0 -0
  112. data/modules/tests/regrooms/aws-iam.yaml +201 -0
  113. data/modules/tests/regrooms/bucket.yml +19 -0
  114. metadata +112 -102
@@ -131,10 +131,10 @@ module MU
131
131
  end
132
132
 
133
133
  # Generic pre-processing of {MU::Config::BasketofKittens::role}, bare and unvalidated.
134
- # @param role [Hash]: The resource to process and validate
135
- # @param configurator [MU::Config]: The overall deployment configurator of which this resource is a member
134
+ # @param _role [Hash]: The resource to process and validate
135
+ # @param _configurator [MU::Config]: The overall deployment configurator of which this resource is a member
136
136
  # @return [Boolean]: True if validation succeeded, False otherwise
137
- def self.validate(role, configurator)
137
+ def self.validate(_role, _configurator)
138
138
  ok = true
139
139
  ok
140
140
  end
@@ -0,0 +1,508 @@
1
+ # Copyright:: Copyright (c) 2014 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
+
17
+ # Methods and structures for parsing Mu's configuration files. See also {MU::Config::BasketofKittens}.
18
+ class Config
19
+
20
+ # The default cloud provider for new resources. Must exist in MU.supportedClouds
21
+ # return [String]
22
+ def self.defaultCloud
23
+ configured = {}
24
+ MU::Cloud.supportedClouds.each { |cloud|
25
+ cloudclass = Object.const_get("MU").const_get("Cloud").const_get(cloud)
26
+ if $MU_CFG[cloud.downcase] and !$MU_CFG[cloud.downcase].empty?
27
+ configured[cloud] = $MU_CFG[cloud.downcase].size
28
+ configured[cloud] += 0.5 if cloudclass.hosted? # tiebreaker
29
+ end
30
+ }
31
+ if configured.size > 0
32
+ return configured.keys.sort { |a, b|
33
+ configured[b] <=> configured[a]
34
+ }.first
35
+ else
36
+ MU::Cloud.supportedClouds.each { |cloud|
37
+ cloudclass = Object.const_get("MU").const_get("Cloud").const_get(cloud)
38
+ return cloud if cloudclass.hosted?
39
+ }
40
+ return MU::Cloud.supportedClouds.first
41
+ end
42
+ end
43
+
44
+ # The default grooming agent for new resources. Must exist in MU.supportedGroomers.
45
+ def self.defaultGroomer
46
+ MU.localOnly ? "Ansible" : "Chef"
47
+ end
48
+
49
+ # Accessor for our Basket of Kittens schema definition
50
+ def self.schema
51
+ @@schema
52
+ end
53
+
54
+ # Deep merge a configuration hash so we can meld different cloud providers'
55
+ # schemas together, while preserving documentation differences
56
+ def self.schemaMerge(orig, new, cloud)
57
+ if new.is_a?(Hash)
58
+ new.each_pair { |k, v|
59
+ if cloud and k == "description" and v.is_a?(String) and !v.match(/\b#{Regexp.quote(cloud.upcase)}\b/) and !v.empty?
60
+ new[k] = "+"+cloud.upcase+"+: "+v
61
+ end
62
+ if orig and orig.has_key?(k)
63
+ elsif orig
64
+ orig[k] = new[k]
65
+ else
66
+ orig = new
67
+ end
68
+ schemaMerge(orig[k], new[k], cloud)
69
+ }
70
+ elsif orig.is_a?(Array) and new
71
+ orig.concat(new)
72
+ orig.uniq!
73
+ elsif new.is_a?(String)
74
+ orig ||= ""
75
+ orig += "\n" if !orig.empty?
76
+ orig += "+#{cloud.upcase}+: "+new
77
+ else
78
+ # XXX I think this is a NOOP?
79
+ end
80
+ end
81
+
82
+ @@allregions = []
83
+ @@loadfails = []
84
+ MU::Cloud.availableClouds.each { |cloud|
85
+ next if @@loadfails.include?(cloud)
86
+ cloudclass = Object.const_get("MU").const_get("Cloud").const_get(cloud)
87
+ begin
88
+ regions = cloudclass.listRegions()
89
+ @@allregions.concat(regions) if regions
90
+ rescue MU::MuError => e
91
+ @@loadfails << cloud
92
+ MU.log e.message, MU::WARN
93
+ end
94
+ }
95
+
96
+ # Configuration chunk for choosing a provider region
97
+ # @return [Hash]
98
+ def self.region_primitive
99
+ if !@@allregions or @@allregions.empty?
100
+ @@allregions = []
101
+ MU::Cloud.availableClouds.each { |cloud|
102
+ next if @@loadfails.include?(cloud)
103
+ cloudclass = Object.const_get("MU").const_get("Cloud").const_get(cloud)
104
+ begin
105
+ return @@allregions if !cloudclass.listRegions()
106
+ @@allregions.concat(cloudclass.listRegions())
107
+ rescue MU::MuError => e
108
+ @@loadfails << cloud
109
+ MU.log e.message, MU::WARN
110
+ end
111
+ }
112
+ end
113
+ {
114
+ "type" => "string",
115
+ "enum" => @@allregions
116
+ }
117
+ end
118
+
119
+ # Configuration chunk for choosing a set of cloud credentials
120
+ # @return [Hash]
121
+ def self.credentials_primitive
122
+ {
123
+ "type" => "string",
124
+ "description" => "Specify a non-default set of credentials to use when authenticating to cloud provider APIs, as listed in `mu.yaml` under each provider's subsection. If "
125
+ }
126
+ end
127
+
128
+ # Configuration chunk for creating resource tags as an array of key/value
129
+ # pairs.
130
+ # @return [Hash]
131
+ def self.optional_tags_primitive
132
+ {
133
+ "type" => "boolean",
134
+ "description" => "Tag the resource with our optional tags (+MU-HANDLE+, +MU-MASTER-NAME+, +MU-OWNER+).",
135
+ "default" => true
136
+ }
137
+ end
138
+
139
+ # Configuration chunk for creating resource tags as an array of key/value
140
+ # pairs.
141
+ # @return [Hash]
142
+ def self.tags_primitive
143
+ {
144
+ "type" => "array",
145
+ "minItems" => 1,
146
+ "items" => {
147
+ "description" => "Tags to apply to this resource. Will apply at the cloud provider level and in node groomers, where applicable.",
148
+ "type" => "object",
149
+ "title" => "tags",
150
+ "required" => ["key", "value"],
151
+ "additionalProperties" => false,
152
+ "properties" => {
153
+ "key" => {
154
+ "type" => "string",
155
+ },
156
+ "value" => {
157
+ "type" => "string",
158
+ }
159
+ }
160
+ }
161
+ }
162
+ end
163
+
164
+ # Configuration chunk for choosing a cloud provider
165
+ # @return [Hash]
166
+ def self.cloud_primitive
167
+ {
168
+ "type" => "string",
169
+ # "default" => MU::Config.defaultCloud, # applyInheritedDefaults does this better
170
+ "enum" => MU::Cloud.supportedClouds
171
+ }
172
+ end
173
+
174
+
175
+ # JSON-schema for resource dependencies
176
+ # @return [Hash]
177
+ def self.dependencies_primitive
178
+ {
179
+ "type" => "array",
180
+ "items" => {
181
+ "type" => "object",
182
+ "description" => "Declare other objects which this resource requires. This resource will wait until the others are available to create itself.",
183
+ "required" => ["name", "type"],
184
+ "additionalProperties" => false,
185
+ "properties" => {
186
+ "name" => {"type" => "string"},
187
+ "type" => {
188
+ "type" => "string",
189
+ "enum" => MU::Cloud.resource_types.values.map { |v| v[:cfg_name] }
190
+ },
191
+ "phase" => {
192
+ "type" => "string",
193
+ "description" => "Which part of the creation process of the resource we depend on should we wait for before starting our own creation? Defaults are usually sensible, but sometimes you want, say, a Server to wait on another Server to be completely ready (through its groom phase) before starting up.",
194
+ "enum" => ["create", "groom"]
195
+ },
196
+ "no_create_wait" => {
197
+ "type" => "boolean",
198
+ "default" => false,
199
+ "description" => "By default, it's assumed that we want to wait on our parents' creation phase, in addition to whatever is declared in this stanza. Setting this flag will bypass waiting on our parent resource's creation, so that our create or groom phase can instead depend only on the parent's groom phase. "
200
+ }
201
+ }
202
+ }
203
+ }
204
+ end
205
+
206
+ # Have a default value available for config schema elements that take an
207
+ # email address.
208
+ # @return [String]
209
+ def self.notification_email
210
+ if MU.chef_user == "mu"
211
+ ENV['MU_ADMIN_EMAIL']
212
+ else
213
+ MU.userEmail
214
+ end
215
+ end
216
+
217
+ # Load and validate the schema for an individual resource class, optionally
218
+ # merging cloud-specific schema components.
219
+ # @param type [String]: The resource type to load
220
+ # @param cloud [String]: A specific cloud, whose implementation's schema of this resource we will merge
221
+ # @return [Hash]
222
+ def self.loadResourceSchema(type, cloud: nil)
223
+ valid = true
224
+ shortclass, _cfg_name, _cfg_plural, _classname = MU::Cloud.getResourceNames(type)
225
+ schemaclass = Object.const_get("MU").const_get("Config").const_get(shortclass)
226
+
227
+ [:schema, :validate].each { |method|
228
+ if !schemaclass.respond_to?(method)
229
+ MU.log "MU::Config::#{type}.#{method.to_s} doesn't seem to be implemented", MU::ERR
230
+ return [nil, false] if method == :schema
231
+ valid = false
232
+ end
233
+ }
234
+
235
+ schema = schemaclass.schema.dup
236
+
237
+ schema["properties"]["virtual_name"] = {
238
+ "description" => "Internal use.",
239
+ "type" => "string"
240
+ }
241
+ schema["properties"]["dependencies"] = MU::Config.dependencies_primitive
242
+ schema["properties"]["cloud"] = MU::Config.cloud_primitive
243
+ schema["properties"]["credentials"] = MU::Config.credentials_primitive
244
+ schema["title"] = type.to_s
245
+
246
+ if cloud
247
+ cloudclass = Object.const_get("MU").const_get("Cloud").const_get(cloud).const_get(shortclass)
248
+
249
+ if cloudclass.respond_to?(:schema)
250
+ _reqd, cloudschema = cloudclass.schema
251
+ cloudschema.each { |key, cfg|
252
+ if schema["properties"][key]
253
+ schemaMerge(schema["properties"][key], cfg, cloud)
254
+ else
255
+ schema["properties"][key] = cfg.dup
256
+ end
257
+ }
258
+ else
259
+ MU.log "MU::Cloud::#{cloud}::#{type}.#{method.to_s} doesn't seem to be implemented", MU::ERR
260
+ valid = false
261
+ end
262
+
263
+ end
264
+
265
+ return [schema, valid]
266
+ end
267
+
268
+ private
269
+
270
+ def applySchemaDefaults(conf_chunk = config, schema_chunk = schema, depth = 0, siblings = nil, type: nil)
271
+ return if schema_chunk.nil?
272
+
273
+ if conf_chunk != nil and schema_chunk["properties"].kind_of?(Hash) and conf_chunk.is_a?(Hash)
274
+
275
+ if schema_chunk["properties"]["creation_style"].nil? or
276
+ schema_chunk["properties"]["creation_style"] != "existing"
277
+ schema_chunk["properties"].each_pair { |key, subschema|
278
+ shortclass = if conf_chunk[key]
279
+ shortclass, _cfg_name, _cfg_plural, _classname = MU::Cloud.getResourceNames(key)
280
+ shortclass
281
+ else
282
+ nil
283
+ end
284
+
285
+ new_val = applySchemaDefaults(conf_chunk[key], subschema, depth+1, conf_chunk, type: shortclass).dup
286
+
287
+ conf_chunk[key] = Marshal.load(Marshal.dump(new_val)) if !new_val.nil?
288
+ }
289
+ end
290
+ elsif schema_chunk["type"] == "array" and conf_chunk.kind_of?(Array)
291
+ conf_chunk.map! { |item|
292
+ # If we're working on a resource type, go get implementation-specific
293
+ # schema information so that we set those defaults correctly.
294
+ realschema = if type and schema_chunk["items"] and schema_chunk["items"]["properties"] and item["cloud"] and MU::Cloud.supportedClouds.include?(item['cloud'])
295
+
296
+ cloudclass = Object.const_get("MU").const_get("Cloud").const_get(item["cloud"]).const_get(type)
297
+ _toplevel_required, cloudschema = cloudclass.schema(self)
298
+
299
+ newschema = schema_chunk["items"].dup
300
+ newschema["properties"].merge!(cloudschema)
301
+ newschema
302
+ else
303
+ schema_chunk["items"].dup
304
+ end
305
+
306
+ applySchemaDefaults(item, realschema, depth+1, conf_chunk, type: type).dup
307
+ }
308
+ else
309
+ if conf_chunk.nil? and !schema_chunk["default_if"].nil? and !siblings.nil?
310
+ schema_chunk["default_if"].each { |cond|
311
+ if siblings[cond["key_is"]] == cond["value_is"]
312
+ return Marshal.load(Marshal.dump(cond["set"]))
313
+ end
314
+ }
315
+ end
316
+ if conf_chunk.nil? and schema_chunk["default"] != nil
317
+ return Marshal.load(Marshal.dump(schema_chunk["default"]))
318
+ end
319
+ end
320
+
321
+ return conf_chunk
322
+ end
323
+
324
+ # Given a bare hash describing a resource, insert default values which can
325
+ # be inherited from its parent or from the root of the BoK.
326
+ # @param kitten [Hash]: A resource descriptor
327
+ # @param type [String]: The type of resource this is ("servers" etc)
328
+ def applyInheritedDefaults(kitten, type)
329
+ return if !kitten.is_a?(Hash)
330
+ kitten['cloud'] ||= @config['cloud']
331
+ kitten['cloud'] ||= MU::Config.defaultCloud
332
+
333
+ if !MU::Cloud.supportedClouds.include?(kitten['cloud'])
334
+ return
335
+ end
336
+
337
+ cloudclass = Object.const_get("MU").const_get("Cloud").const_get(kitten['cloud'])
338
+ shortclass, _cfg_name, _cfg_plural, _classname = MU::Cloud.getResourceNames(type)
339
+ resclass = Object.const_get("MU").const_get("Cloud").const_get(kitten['cloud']).const_get(shortclass)
340
+
341
+ schema_fields = ["us_only", "scrub_mu_isms", "credentials", "billing_acct"]
342
+ if !resclass.isGlobal?
343
+ kitten['region'] ||= @config['region']
344
+ kitten['region'] ||= cloudclass.myRegion(kitten['credentials'])
345
+ schema_fields << "region"
346
+ end
347
+
348
+ kitten['credentials'] ||= @config['credentials']
349
+ kitten['credentials'] ||= cloudclass.credConfig(name_only: true)
350
+
351
+ kitten['us_only'] ||= @config['us_only']
352
+ kitten['us_only'] ||= false
353
+
354
+ kitten['scrub_mu_isms'] ||= @config['scrub_mu_isms']
355
+ kitten['scrub_mu_isms'] ||= false
356
+
357
+ if kitten['cloud'] == "Google"
358
+ # TODO this should be cloud-generic (handle AWS accounts, Azure subscriptions)
359
+ if resclass.canLiveIn.include?(:Habitat)
360
+ kitten["project"] ||= MU::Cloud::Google.defaultProject(kitten['credentials'])
361
+ schema_fields << "project"
362
+ end
363
+ if kitten['region'].nil? and !kitten['#MU_CLOUDCLASS'].nil? and
364
+ !resclass.isGlobal? and
365
+ ![MU::Cloud::VPC, MU::Cloud::FirewallRule].include?(kitten['#MU_CLOUDCLASS'])
366
+ if MU::Cloud::Google.myRegion((kitten['credentials'])).nil?
367
+ raise ValidationError, "Google '#{type}' resource '#{kitten['name']}' declared without a region, but no default Google region declared in mu.yaml under #{kitten['credentials'].nil? ? "default" : kitten['credentials']} credential set"
368
+ end
369
+ kitten['region'] ||= MU::Cloud::Google.myRegion
370
+ end
371
+ elsif kitten["cloud"] == "AWS" and !resclass.isGlobal? and !kitten['region']
372
+ if MU::Cloud::AWS.myRegion.nil?
373
+ raise ValidationError, "AWS resource declared without a region, but no default AWS region found"
374
+ end
375
+ kitten['region'] ||= MU::Cloud::AWS.myRegion
376
+ end
377
+
378
+
379
+ kitten['billing_acct'] ||= @config['billing_acct'] if @config['billing_acct']
380
+
381
+ kitten["dependencies"] ||= []
382
+
383
+ # Make sure the schema knows about these "new" fields, so that validation
384
+ # doesn't trip over them.
385
+ schema_fields.each { |field|
386
+ if @@schema["properties"][field]
387
+ MU.log "Adding #{field} to schema for #{type} #{kitten['cloud']}", MU::DEBUG, details: @@schema["properties"][field]
388
+ @@schema["properties"][type]["items"]["properties"][field] ||= @@schema["properties"][field]
389
+ end
390
+ }
391
+ end
392
+
393
+ CIDR_PATTERN = "^\\d+\\.\\d+\\.\\d+\\.\\d+\/[0-9]{1,2}$"
394
+ CIDR_DESCRIPTION = "CIDR-formatted IP block, e.g. 1.2.3.4/32"
395
+ CIDR_PRIMITIVE = {
396
+ "type" => "string",
397
+ "pattern" => CIDR_PATTERN,
398
+ "description" => CIDR_DESCRIPTION
399
+ }
400
+
401
+
402
+ @@schema = {
403
+ "$schema" => "http://json-schema.org/draft-04/schema#",
404
+ "title" => "MU Application",
405
+ "type" => "object",
406
+ "description" => "A MU application stack, consisting of at least one resource.",
407
+ "required" => ["admins", "appname"],
408
+ "properties" => {
409
+ "appname" => {
410
+ "type" => "string",
411
+ "description" => "A name for your application stack. Should be short, but easy to differentiate from other applications.",
412
+ },
413
+ "scrub_mu_isms" => {
414
+ "type" => "boolean",
415
+ "description" => "When 'cloud' is set to 'CloudFormation,' use this flag to strip out Mu-specific artifacts (tags, standard userdata, naming conventions, etc) to yield a clean, source-agnostic template. Setting this flag here will override declarations in individual resources."
416
+ },
417
+ "project" => {
418
+ "type" => "string",
419
+ "description" => "**GOOGLE ONLY**: The project into which to deploy resources"
420
+ },
421
+ "billing_acct" => {
422
+ "type" => "string",
423
+ "description" => "**GOOGLE ONLY**: Billing account ID to associate with a newly-created Google Project. If not specified, will attempt to locate a billing account associated with the default project for our credentials.",
424
+ },
425
+ "region" => MU::Config.region_primitive,
426
+ "credentials" => MU::Config.credentials_primitive,
427
+ "us_only" => {
428
+ "type" => "boolean",
429
+ "description" => "For resources which span regions, restrict to regions inside the United States",
430
+ "default" => false
431
+ },
432
+ "conditions" => {
433
+ "type" => "array",
434
+ "items" => {
435
+ "type" => "object",
436
+ "required" => ["name", "cloudcode"],
437
+ "description" => "CloudFormation-specific. Define Conditions as in http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/conditions-section-structure.html. Arguments must use the cloudCode() macro.",
438
+ "properties" => {
439
+ "name" => { "required" => true, "type" => "string" },
440
+ "cloudcode" => { "required" => true, "type" => "string" },
441
+ }
442
+ }
443
+ },
444
+ "parameters" => {
445
+ "type" => "array",
446
+ "items" => {
447
+ "type" => "object",
448
+ "title" => "parameter",
449
+ "description" => "Parameters to be substituted elsewhere in this Basket of Kittens as ERB variables (<%= varname %>)",
450
+ "additionalProperties" => false,
451
+ "properties" => {
452
+ "name" => { "required" => true, "type" => "string" },
453
+ "default" => { "type" => "string" },
454
+ "list_of" => {
455
+ "type" => "string",
456
+ "description" => "Treat the value as a comma-separated list of values with this key name, equivalent to CloudFormation's various List<> types. For example, set to 'subnet_id' to pass values as an array of subnet identifiers as the 'subnets' argument of a VPC stanza."
457
+ },
458
+ "prettyname" => {
459
+ "type" => "string",
460
+ "description" => "An alternative name to use when generating parameter fields in, for example, CloudFormation templates"
461
+ },
462
+ "description" => {"type" => "string"},
463
+ "cloudtype" => {
464
+ "type" => "string",
465
+ "description" => "A platform-specific string describing the type of validation to use for this parameter. E.g. when generating a CloudFormation template, set to AWS::EC2::Image::Id to validate input as an AMI identifier."
466
+ },
467
+ "required" => {
468
+ "type" => "boolean",
469
+ "default" => true
470
+ },
471
+ "valid_values" => {
472
+ "type" => "array",
473
+ "description" => "List of valid values for this parameter. Can only be a list of static strings, for now.",
474
+ "items" => {
475
+ "type" => "string"
476
+ }
477
+ }
478
+ }
479
+ }
480
+ },
481
+ # TODO availability zones (or an array thereof)
482
+
483
+ "admins" => {
484
+ "type" => "array",
485
+ "items" => {
486
+ "type" => "object",
487
+ "title" => "admin",
488
+ "description" => "Administrative contacts for this application stack. Will be automatically set to invoking Mu user, if not specified.",
489
+ "required" => ["name", "email"],
490
+ "additionalProperties" => false,
491
+ "properties" => {
492
+ "name" => {"type" => "string"},
493
+ "email" => {"type" => "string"},
494
+ "public_key" => {
495
+ "type" => "string",
496
+ "description" => "An OpenSSH-style public key string. This will be installed on all instances created in this deployment."
497
+ }
498
+ }
499
+ },
500
+ "minItems" => 1,
501
+ "uniqueItems" => true
502
+ }
503
+ },
504
+ "additionalProperties" => false
505
+ }
506
+
507
+ end #class
508
+ end #module