cumuliform 0.4.0 → 0.5.1

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: dc898679239945b8548cd8b35fac0135ca1c07d4
4
- data.tar.gz: 5e5996b0de7f64ba2deb495dcf306374af28947c
3
+ metadata.gz: abff1bdeb60a617a79f889e191789cc21dee2335
4
+ data.tar.gz: 8320b16dd011e3155aaeef164a4dce008634e46d
5
5
  SHA512:
6
- metadata.gz: a1de59e26464fb7370af9cdac344ba606ababdc27ec608d0324cf976352c4eded31b1e78731d4ed40c4282029deb0f3b15b3e42a96e70d9c06bb782c310100a9
7
- data.tar.gz: 5511163114e278987878e18844961a03fd8223fb6e20d328bc1653a194c87f3955ec9b5f74257b49f4fbfcb0561a91adecc0fddebe7fac956d07176bc92c0f3a
6
+ metadata.gz: 49b1742f90f29be0daacbd3c452cdda449100bc452ec831466661639c8fc28402e2cbdf833a04c5d4cecf1eeee97e4ccd03bf4717943032a4eb2813c407df54c
7
+ data.tar.gz: 20147ec9d4c9790d2ac5fc5fb78c5f3126582e2ed7c86c698be3bf25b6b7cf49bc625759e7a2e07d93309510fc1dba25f714ca1a2dc573e53eb514cce13f26f4
data/CHANGELOG.md ADDED
@@ -0,0 +1,31 @@
1
+ # Changelog
2
+ All notable changes to this project will be documented in this file.
3
+ This project adheres to [Semantic Versioning](http://semver.org/).
4
+
5
+ ## [0.5.1] - 2015-12-03
6
+ ### Changed
7
+ - Make some fragment-related functions private
8
+
9
+ ### Deprecate
10
+ - Deprecate fragment definition use of `fragment()`: use `def_fragment()` for
11
+ that instead
12
+
13
+ ### Added
14
+ - Add `def_fragment()` replacement for overloaded fragment-definition use of
15
+ `fragment()`
16
+ - Better API doc and examples for intrinsic functions
17
+ - API doc and examples for fragments
18
+
19
+ ## [0.5.0] - 2015-12-03
20
+ Yanked - implemented 0.5.1's changes in a breaking way rather than deprecating
21
+
22
+ ## [0.4.0] - 2015-06-10
23
+ ### Added
24
+ - Add example to README
25
+ - Add command-line runner
26
+ - Add Rake task class
27
+
28
+ ## [0.3.0] - 2015-05-27
29
+ ### Added
30
+ - Initial public release covering all the basics (missing a couple of intrinsic
31
+ functions)
data/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Cumuliform
2
2
 
3
- [![Build Status](https://travis-ci.org/tape-tv/cumuliform.svg?branch=master)](https://travis-ci.org/tape-tv/cumuliform) [![Code Climate](https://codeclimate.com/github/tape-tv/cumuliform/badges/gpa.svg)](https://codeclimate.com/github/tape-tv/cumuliform) [![Test Coverage](https://codeclimate.com/github/tape-tv/cumuliform/badges/coverage.svg)](https://codeclimate.com/github/tape-tv/cumuliform/coverage)
3
+ [![Gem Version](https://badge.fury.io/rb/cumuliform.svg)](http://badge.fury.io/rb/cumuliform) [![Build Status](https://travis-ci.org/tape-tv/cumuliform.svg?branch=master)](https://travis-ci.org/tape-tv/cumuliform) [![Code Climate](https://codeclimate.com/github/tape-tv/cumuliform/badges/gpa.svg)](https://codeclimate.com/github/tape-tv/cumuliform) [![Test Coverage](https://codeclimate.com/github/tape-tv/cumuliform/badges/coverage.svg)](https://codeclimate.com/github/tape-tv/cumuliform/coverage)
4
4
 
5
5
  Amazon’s [CloudFormation AWS service][cf] provides a way to describe
6
6
  infrastructure stacks using a JSON template. We love CloudFormation, and use it
@@ -120,6 +120,8 @@ $ cumuliform simplest.rb simplest.cform
120
120
  }
121
121
  ```
122
122
 
123
+ More detailed examples are below the section on Rake...
124
+
123
125
  # Rake tasks and the Command Line runner
124
126
 
125
127
  Cumuliform provides a very simple command-line runner to turn a `.rb` template
@@ -165,6 +167,617 @@ TARGETS = Rake::FileList['*.rb'].ext('.cform')
165
167
  task :cform => TARGETS
166
168
  ```
167
169
 
170
+ # Examples
171
+
172
+ ## Simple top-level object declarations
173
+
174
+ This example declares one of each of the top level objects Cumuliform
175
+ supports. More details can be found in CloudFormation's [Template
176
+ Anatomy documentation][cf-ta].
177
+
178
+ [cf-ta]: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/template-anatomy.html
179
+
180
+ ```ruby
181
+ Cumuliform.template do
182
+ # See http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/parameters-section-structure.html
183
+ parameter 'AMI' do
184
+ {
185
+ Description: 'The AMI id for our template (defaults to the stock Ubuntu 14.04 image in eu-central-1)',
186
+ Type: 'String',
187
+ Default: 'ami-accff2b1'
188
+ }
189
+ end
190
+
191
+ # See http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/mappings-section-structure.html
192
+ mapping 'RegionAMI' do
193
+ {
194
+ 'eu-central-1' => {
195
+ 'hvm' => 'ami-accff2b1',
196
+ 'pv' => 'ami-b6cff2ab'
197
+ },
198
+ 'eu-west-1' => {
199
+ 'hvm' => 'ami-47a23a30',
200
+ 'pv' => 'ami-5da23a2a'
201
+ }
202
+ }
203
+ end
204
+
205
+ # See http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/conditions-section-structure.html
206
+ condition 'Ireland' do
207
+ fn.equals(ref('AWS::Region'), 'eu-west-1')
208
+ end
209
+
210
+ # See http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/resources-section-structure.html
211
+ resource 'PrimaryInstance' do
212
+ {
213
+ Type: 'AWS::EC2::Instance',
214
+ Properties: {
215
+ ImageId: ref('AMI'),
216
+ InstanceType: 'm3.medium'
217
+ }
218
+ }
219
+ end
220
+
221
+ # See http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/outputs-section-structure.html
222
+ output 'PrimaryInstanceID' do
223
+ {
224
+ Value: ref('PrimaryInstance')
225
+ }
226
+ end
227
+ end
228
+ ```
229
+
230
+ The generated template is:
231
+
232
+ ```json
233
+ {
234
+ "Parameters": {
235
+ "AMI": {
236
+ "Description": "The AMI id for our template (defaults to the stock Ubuntu 14.04 image in eu-central-1)",
237
+ "Type": "String",
238
+ "Default": "ami-accff2b1"
239
+ }
240
+ },
241
+ "Mappings": {
242
+ "RegionAMI": {
243
+ "eu-central-1": {
244
+ "hvm": "ami-accff2b1",
245
+ "pv": "ami-b6cff2ab"
246
+ },
247
+ "eu-west-1": {
248
+ "hvm": "ami-47a23a30",
249
+ "pv": "ami-5da23a2a"
250
+ }
251
+ }
252
+ },
253
+ "Conditions": {
254
+ "Ireland": {
255
+ "Fn::Equals": [
256
+ {
257
+ "Ref": "AWS::Region"
258
+ },
259
+ "eu-west-1"
260
+ ]
261
+ }
262
+ },
263
+ "Resources": {
264
+ "PrimaryInstance": {
265
+ "Type": "AWS::EC2::Instance",
266
+ "Properties": {
267
+ "ImageId": {
268
+ "Ref": "AMI"
269
+ },
270
+ "InstanceType": "m3.medium"
271
+ }
272
+ }
273
+ },
274
+ "Outputs": {
275
+ "PrimaryInstanceID": {
276
+ "Value": {
277
+ "Ref": "PrimaryInstance"
278
+ }
279
+ }
280
+ }
281
+ }
282
+ ```
283
+
284
+ Note that the optional `AWSTemplateFormatVersion`, `Description`, and
285
+ `Metadata` sections are *not* currently supported.
286
+
287
+ ## Intrinsic functions
288
+
289
+ Cumuliform provides convenience wrappers for all the intrinsic functions. See
290
+ CloudFormation's [Intrinsic Function documentation][cf-if].
291
+
292
+ [cf-if]: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference.html
293
+
294
+ ```ruby
295
+ Cumuliform.template do
296
+ mapping 'RegionAMI' do
297
+ {
298
+ 'eu-central-1' => {
299
+ 'hvm' => 'ami-accff2b1',
300
+ 'pv' => 'ami-b6cff2ab'
301
+ },
302
+ 'eu-west-1' => {
303
+ 'hvm' => 'ami-47a23a30',
304
+ 'pv' => 'ami-5da23a2a'
305
+ }
306
+ }
307
+ end
308
+
309
+ parameter 'VirtualizationMethod' do
310
+ {
311
+ Type: 'String',
312
+ Default: 'hvm'
313
+ }
314
+ end
315
+
316
+ resource 'PrimaryInstance' do
317
+ {
318
+ Type: 'AWS::EC2::Instance',
319
+ Properties: {
320
+ ImageId: fn.find_in_map('RegionAMI', ref('AWS::Region'),
321
+ ref('VirtualizationMethod')),
322
+ InstanceType: 'm3.medium',
323
+ AvailabilityZone: fn.select(0, fn.get_azs),
324
+ UserData: fn.base64(
325
+ fn.join('', [
326
+ "#!/bin/bash -xe\n",
327
+ "apt-get update\n",
328
+ "apt-get -y install python-pip python-docutils\n",
329
+ "pip install https://s3.amazonaws.com/cloudformation-examples/aws-cfn-bootstrap-latest.tar.gz\n",
330
+ "/usr/local/bin/cfn-init",
331
+ " --region ", ref("AWS::Region"),
332
+ " --stack ", ref("AWS::StackId"),
333
+ " --resource #{xref('PrimaryInstance')}",
334
+ " --configsets db"
335
+ ])
336
+ ),
337
+ Metadata: {
338
+ 'AWS::CloudFormation::Init' => {
339
+ configSets: { db: ['install'] },
340
+ install: {
341
+ commands: {
342
+ '01-apt' => {
343
+ command: 'apt-get install postgresql postgresql-contrib'
344
+ },
345
+ '02-db' => {
346
+ command: 'sudo -u postgres createdb the-db'
347
+ }
348
+ }
349
+ }
350
+ }
351
+ }
352
+ }
353
+ }
354
+ end
355
+
356
+ resource 'SiteDNS' do
357
+ {
358
+ Type: "AWS::Route53::RecordSet",
359
+ Properties: {
360
+ HostedZoneName: 'my-zone.example.org',
361
+ Name: 'service.my-zone.example.org',
362
+ ResourceRecords: [fn.get_att(xref('LoadBalancer'), 'DNSName')],
363
+ TTL: '900',
364
+ Type: 'CNAME'
365
+ }
366
+ }
367
+ end
368
+
369
+ resource 'LoadBalancer' do
370
+ {
371
+ Type: 'AWS::ElasticLoadBalancing::LoadBalancer',
372
+ Properties: {
373
+ AvailabilityZones: [fn.select(0, fn.get_azs)],
374
+ Listeners: [
375
+ {
376
+ InstancePort: 5432,
377
+ Protocol: 'TCP'
378
+ }
379
+ ],
380
+ Instances: [ref('PrimaryInstance')]
381
+ }
382
+ }
383
+ end
384
+ end
385
+ ```
386
+
387
+ The generated template is:
388
+
389
+ ```json
390
+ {
391
+ "Parameters": {
392
+ "VirtualizationMethod": {
393
+ "Type": "String",
394
+ "Default": "hvm"
395
+ }
396
+ },
397
+ "Mappings": {
398
+ "RegionAMI": {
399
+ "eu-central-1": {
400
+ "hvm": "ami-accff2b1",
401
+ "pv": "ami-b6cff2ab"
402
+ },
403
+ "eu-west-1": {
404
+ "hvm": "ami-47a23a30",
405
+ "pv": "ami-5da23a2a"
406
+ }
407
+ }
408
+ },
409
+ "Resources": {
410
+ "PrimaryInstance": {
411
+ "Type": "AWS::EC2::Instance",
412
+ "Properties": {
413
+ "ImageId": {
414
+ "Fn::FindInMap": [
415
+ "RegionAMI",
416
+ {
417
+ "Ref": "AWS::Region"
418
+ },
419
+ {
420
+ "Ref": "VirtualizationMethod"
421
+ }
422
+ ]
423
+ },
424
+ "InstanceType": "m3.medium",
425
+ "AvailabilityZone": {
426
+ "Fn::Select": [
427
+ "0",
428
+ {
429
+ "Fn::GetAZs": ""
430
+ }
431
+ ]
432
+ },
433
+ "UserData": {
434
+ "Fn::Base64": {
435
+ "Fn::Join": [
436
+ "",
437
+ [
438
+ "#!/bin/bash -xe\n",
439
+ "apt-get update\n",
440
+ "apt-get -y install python-pip python-docutils\n",
441
+ "pip install https://s3.amazonaws.com/cloudformation-examples/aws-cfn-bootstrap-latest.tar.gz\n",
442
+ "/usr/local/bin/cfn-init",
443
+ " --region ",
444
+ {
445
+ "Ref": "AWS::Region"
446
+ },
447
+ " --stack ",
448
+ {
449
+ "Ref": "AWS::StackId"
450
+ },
451
+ " --resource PrimaryInstance",
452
+ " --configsets db"
453
+ ]
454
+ ]
455
+ }
456
+ },
457
+ "Metadata": {
458
+ "AWS::CloudFormation::Init": {
459
+ "configSets": {
460
+ "db": [
461
+ "install"
462
+ ]
463
+ },
464
+ "install": {
465
+ "commands": {
466
+ "01-apt": {
467
+ "command": "apt-get install postgresql postgresql-contrib"
468
+ },
469
+ "02-db": {
470
+ "command": "sudo -u postgres createdb the-db"
471
+ }
472
+ }
473
+ }
474
+ }
475
+ }
476
+ }
477
+ },
478
+ "SiteDNS": {
479
+ "Type": "AWS::Route53::RecordSet",
480
+ "Properties": {
481
+ "HostedZoneName": "my-zone.example.org",
482
+ "Name": "service.my-zone.example.org",
483
+ "ResourceRecords": [
484
+ {
485
+ "Fn::GetAtt": [
486
+ "LoadBalancer",
487
+ "DNSName"
488
+ ]
489
+ }
490
+ ],
491
+ "TTL": "900",
492
+ "Type": "CNAME"
493
+ }
494
+ },
495
+ "LoadBalancer": {
496
+ "Type": "AWS::ElasticLoadBalancing::LoadBalancer",
497
+ "Properties": {
498
+ "AvailabilityZones": [
499
+ {
500
+ "Fn::Select": [
501
+ "0",
502
+ {
503
+ "Fn::GetAZs": ""
504
+ }
505
+ ]
506
+ }
507
+ ],
508
+ "Listeners": [
509
+ {
510
+ "InstancePort": 5432,
511
+ "Protocol": "TCP"
512
+ }
513
+ ],
514
+ "Instances": [
515
+ {
516
+ "Ref": "PrimaryInstance"
517
+ }
518
+ ]
519
+ }
520
+ }
521
+ }
522
+ }
523
+ ```
524
+
525
+ ## Condition functions
526
+
527
+ Cumuliform provides convenience wrappers for all the Condition-related intrinsic functions. See
528
+ CloudFormation's [Condition Function documentation][cf-cif].
529
+
530
+ [cf-cif]: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-conditions.html
531
+
532
+ ```ruby
533
+ Cumuliform.template do
534
+ parameter 'AMI' do
535
+ {
536
+ Type: 'String',
537
+ Default: 'ami-12345678'
538
+ }
539
+ end
540
+
541
+ parameter 'UtilAMI' do
542
+ {
543
+ Type: 'String',
544
+ Default: 'ami-abcdef12'
545
+ }
546
+ end
547
+
548
+ parameter 'InstanceType' do
549
+ {
550
+ Description: "The instance type",
551
+ Type: 'String',
552
+ Default: 'c4.large'
553
+ }
554
+ end
555
+
556
+ condition 'InEU' do
557
+ fn.or(
558
+ fn.equals('eu-central-1', ref('AWS::Region')),
559
+ fn.equals('eu-west-1', ref('AWS::Region'))
560
+ )
561
+ end
562
+
563
+ condition 'UtilBox' do
564
+ fn.and(
565
+ fn.equals('m4.large', ref('InstanceType')),
566
+ {"Condition": xref('InEU')}
567
+ )
568
+ end
569
+
570
+ condition 'WebBox' do
571
+ fn.not(ref('UtilBox'))
572
+ end
573
+
574
+ resource 'WebInstance' do
575
+ {
576
+ Type: "AWS::EC2::Instance",
577
+ Condition: xref('WebBox'),
578
+ Properties: {
579
+ ImageId: ref('AMI'),
580
+ InstanceType: ref('InstanceType')
581
+ }
582
+ }
583
+ end
584
+
585
+ resource 'UtilInstance' do
586
+ {
587
+ Type: "AWS::EC2::Instance",
588
+ Condition: xref('UtilBox'),
589
+ Properties: {
590
+ ImageId: ref('UtilAMI'),
591
+ InstanceType: ref('InstanceType')
592
+ }
593
+ }
594
+ end
595
+ end
596
+ ```
597
+
598
+ The generated template is:
599
+
600
+ ```json
601
+ {
602
+ "Parameters": {
603
+ "AMI": {
604
+ "Type": "String",
605
+ "Default": "ami-12345678"
606
+ },
607
+ "UtilAMI": {
608
+ "Type": "String",
609
+ "Default": "ami-abcdef12"
610
+ },
611
+ "InstanceType": {
612
+ "Description": "The instance type",
613
+ "Type": "String",
614
+ "Default": "c4.large"
615
+ }
616
+ },
617
+ "Conditions": {
618
+ "InEU": {
619
+ "Fn::Or": [
620
+ {
621
+ "Fn::Equals": [
622
+ "eu-central-1",
623
+ {
624
+ "Ref": "AWS::Region"
625
+ }
626
+ ]
627
+ },
628
+ {
629
+ "Fn::Equals": [
630
+ "eu-west-1",
631
+ {
632
+ "Ref": "AWS::Region"
633
+ }
634
+ ]
635
+ }
636
+ ]
637
+ },
638
+ "UtilBox": {
639
+ "Fn::And": [
640
+ {
641
+ "Fn::Equals": [
642
+ "m4.large",
643
+ {
644
+ "Ref": "InstanceType"
645
+ }
646
+ ]
647
+ },
648
+ {
649
+ "Condition": "InEU"
650
+ }
651
+ ]
652
+ },
653
+ "WebBox": {
654
+ "Fn::Not": [
655
+ {
656
+ "Ref": "UtilBox"
657
+ }
658
+ ]
659
+ }
660
+ },
661
+ "Resources": {
662
+ "WebInstance": {
663
+ "Type": "AWS::EC2::Instance",
664
+ "Condition": "WebBox",
665
+ "Properties": {
666
+ "ImageId": {
667
+ "Ref": "AMI"
668
+ },
669
+ "InstanceType": {
670
+ "Ref": "InstanceType"
671
+ }
672
+ }
673
+ },
674
+ "UtilInstance": {
675
+ "Type": "AWS::EC2::Instance",
676
+ "Condition": "UtilBox",
677
+ "Properties": {
678
+ "ImageId": {
679
+ "Ref": "UtilAMI"
680
+ },
681
+ "InstanceType": {
682
+ "Ref": "InstanceType"
683
+ }
684
+ }
685
+ }
686
+ }
687
+ }
688
+ ```
689
+
690
+ ### xref
691
+ Quite often you'll need to use a Resource, Condition, or Parameter Logical ID
692
+ outside of a `{ "Ref" => "LogicalID" }`. Because Logical IDs are one of the
693
+ things we *can* check at evaluation time, we provide a function that simply
694
+ takes a Logical ID, checks it, then returns it. If the Logical ID isn't there
695
+ then it explodes with a `Cumuliform::Error::NoSuchLogicalId`.
696
+
697
+ ```ruby
698
+ resource "Resource" do
699
+ {
700
+ Type: "AWS::EC2::Instance",
701
+ Condition: xref("TheCondition")
702
+ }
703
+ end
704
+ ```
705
+
706
+ ## Fragments
707
+ You'll often want to use a collection of resources several times in a template, and it can be pretty verbose and tedious. Cumuliform offers reusable fragments to allow you to reuse similar template chunks.
708
+
709
+ You define them with `def_fragment()` and use them with `fragment()`. You pass a name and a block to `def_fragment`. You call `fragment()` with the name of a fragment and an optional hash of options to pass to the fragment block. The fragment block is called and its return value output into the template.
710
+
711
+ Here's an example:
712
+
713
+ ```ruby
714
+ Cumuliform.template do
715
+ parameter 'AMI' do
716
+ {
717
+ Description: 'The AMI id for our template (defaults to the stock Ubuntu 14.04 image in eu-central-1)',
718
+ Type: 'String',
719
+ Default: 'ami-accff2b1'
720
+ }
721
+ end
722
+
723
+ def_fragment(:instance) do |opts|
724
+ resource opts[:logical_id] do
725
+ {
726
+ Type: 'AWS::EC2::Instance',
727
+ Properties: {
728
+ ImageId: ref('AMI'),
729
+ InstanceType: opts[:instance_type]
730
+ }
731
+ }
732
+ end
733
+ end
734
+
735
+ fragment(:instance, logical_id: 'LittleInstance', instance_type: 't2.micro')
736
+ fragment(:instance, logical_id: 'BigInstance', instance_type: 'c4.xlarge')
737
+ end
738
+ ```
739
+
740
+ And the output:
741
+
742
+ ```json
743
+ {
744
+ "Parameters": {
745
+ "AMI": {
746
+ "Description": "The AMI id for our template (defaults to the stock Ubuntu 14.04 image in eu-central-1)",
747
+ "Type": "String",
748
+ "Default": "ami-accff2b1"
749
+ }
750
+ },
751
+ "Resources": {
752
+ "LittleInstance": {
753
+ "Type": "AWS::EC2::Instance",
754
+ "Properties": {
755
+ "ImageId": {
756
+ "Ref": "AMI"
757
+ },
758
+ "InstanceType": "t2.micro"
759
+ }
760
+ },
761
+ "BigInstance": {
762
+ "Type": "AWS::EC2::Instance",
763
+ "Properties": {
764
+ "ImageId": {
765
+ "Ref": "AMI"
766
+ },
767
+ "InstanceType": "c4.xlarge"
768
+ }
769
+ }
770
+ }
771
+ }
772
+ ```
773
+
774
+ ## Importing other templates
775
+ _TODO_
776
+
777
+ ## Helpers
778
+ _TODO_
779
+
780
+
168
781
  # Development
169
782
 
170
783
  After checking out the repo, run `bin/setup` to install dependencies. Then, run
data/Rakefile CHANGED
@@ -14,4 +14,5 @@ require 'cumuliform/rake_task'
14
14
 
15
15
  Cumuliform::RakeTask.rule(".cform" => ".rb")
16
16
 
17
+ desc "Generate JSON from example templates"
17
18
  task :examples => EXAMPLE_TARGETS
data/cumuliform.gemspec CHANGED
@@ -11,7 +11,7 @@ Gem::Specification.new do |spec|
11
11
 
12
12
  spec.summary = %q{DSL library for generating AWS CloudFormation templates}
13
13
  spec.description = <<-EOD
14
- Simple DSL for generation AWS CloudFormation templates with an emphasis
14
+ Simple DSL for generating AWS CloudFormation templates with an emphasis
15
15
  on ensuring you don't shoot yourself in the foot by, e.g. referencing
16
16
  non-existent resources because you have a typo.
17
17
  EOD
@@ -26,4 +26,5 @@ non-existent resources because you have a typo.
26
26
  spec.add_development_dependency "bundler", "~> 1.9"
27
27
  spec.add_development_dependency "rake", "~> 10.0"
28
28
  spec.add_development_dependency "rspec", "~> 3"
29
+ spec.add_development_dependency "yard", ">= 0.8"
29
30
  end
@@ -0,0 +1,63 @@
1
+ Cumuliform.template do
2
+ parameter 'AMI' do
3
+ {
4
+ Type: 'String',
5
+ Default: 'ami-12345678'
6
+ }
7
+ end
8
+
9
+ parameter 'UtilAMI' do
10
+ {
11
+ Type: 'String',
12
+ Default: 'ami-abcdef12'
13
+ }
14
+ end
15
+
16
+ parameter 'InstanceType' do
17
+ {
18
+ Description: "The instance type",
19
+ Type: 'String',
20
+ Default: 'c4.large'
21
+ }
22
+ end
23
+
24
+ condition 'InEU' do
25
+ fn.or(
26
+ fn.equals('eu-central-1', ref('AWS::Region')),
27
+ fn.equals('eu-west-1', ref('AWS::Region'))
28
+ )
29
+ end
30
+
31
+ condition 'UtilBox' do
32
+ fn.and(
33
+ fn.equals('m4.large', ref('InstanceType')),
34
+ {"Condition": xref('InEU')}
35
+ )
36
+ end
37
+
38
+ condition 'WebBox' do
39
+ fn.not(ref('UtilBox'))
40
+ end
41
+
42
+ resource 'WebInstance' do
43
+ {
44
+ Type: "AWS::EC2::Instance",
45
+ Condition: xref('WebBox'),
46
+ Properties: {
47
+ ImageId: ref('AMI'),
48
+ InstanceType: ref('InstanceType')
49
+ }
50
+ }
51
+ end
52
+
53
+ resource 'UtilInstance' do
54
+ {
55
+ Type: "AWS::EC2::Instance",
56
+ Condition: xref('UtilBox'),
57
+ Properties: {
58
+ ImageId: ref('UtilAMI'),
59
+ InstanceType: ref('InstanceType')
60
+ }
61
+ }
62
+ end
63
+ end
@@ -0,0 +1,24 @@
1
+ Cumuliform.template do
2
+ parameter 'AMI' do
3
+ {
4
+ Description: 'The AMI id for our template (defaults to the stock Ubuntu 14.04 image in eu-central-1)',
5
+ Type: 'String',
6
+ Default: 'ami-accff2b1'
7
+ }
8
+ end
9
+
10
+ def_fragment(:instance) do |opts|
11
+ resource opts[:logical_id] do
12
+ {
13
+ Type: 'AWS::EC2::Instance',
14
+ Properties: {
15
+ ImageId: ref('AMI'),
16
+ InstanceType: opts[:instance_type]
17
+ }
18
+ }
19
+ end
20
+ end
21
+
22
+ fragment(:instance, logical_id: 'LittleInstance', instance_type: 't2.micro')
23
+ fragment(:instance, logical_id: 'BigInstance', instance_type: 'c4.xlarge')
24
+ end
@@ -0,0 +1,90 @@
1
+ Cumuliform.template do
2
+ mapping 'RegionAMI' do
3
+ {
4
+ 'eu-central-1' => {
5
+ 'hvm' => 'ami-accff2b1',
6
+ 'pv' => 'ami-b6cff2ab'
7
+ },
8
+ 'eu-west-1' => {
9
+ 'hvm' => 'ami-47a23a30',
10
+ 'pv' => 'ami-5da23a2a'
11
+ }
12
+ }
13
+ end
14
+
15
+ parameter 'VirtualizationMethod' do
16
+ {
17
+ Type: 'String',
18
+ Default: 'hvm'
19
+ }
20
+ end
21
+
22
+ resource 'PrimaryInstance' do
23
+ {
24
+ Type: 'AWS::EC2::Instance',
25
+ Properties: {
26
+ ImageId: fn.find_in_map('RegionAMI', ref('AWS::Region'),
27
+ ref('VirtualizationMethod')),
28
+ InstanceType: 'm3.medium',
29
+ AvailabilityZone: fn.select(0, fn.get_azs),
30
+ UserData: fn.base64(
31
+ fn.join('', [
32
+ "#!/bin/bash -xe\n",
33
+ "apt-get update\n",
34
+ "apt-get -y install python-pip python-docutils\n",
35
+ "pip install https://s3.amazonaws.com/cloudformation-examples/aws-cfn-bootstrap-latest.tar.gz\n",
36
+ "/usr/local/bin/cfn-init",
37
+ " --region ", ref("AWS::Region"),
38
+ " --stack ", ref("AWS::StackId"),
39
+ " --resource #{xref('PrimaryInstance')}",
40
+ " --configsets db"
41
+ ])
42
+ ),
43
+ Metadata: {
44
+ 'AWS::CloudFormation::Init' => {
45
+ configSets: { db: ['install'] },
46
+ install: {
47
+ commands: {
48
+ '01-apt' => {
49
+ command: 'apt-get install postgresql postgresql-contrib'
50
+ },
51
+ '02-db' => {
52
+ command: 'sudo -u postgres createdb the-db'
53
+ }
54
+ }
55
+ }
56
+ }
57
+ }
58
+ }
59
+ }
60
+ end
61
+
62
+ resource 'SiteDNS' do
63
+ {
64
+ Type: "AWS::Route53::RecordSet",
65
+ Properties: {
66
+ HostedZoneName: 'my-zone.example.org',
67
+ Name: 'service.my-zone.example.org',
68
+ ResourceRecords: [fn.get_att(xref('LoadBalancer'), 'DNSName')],
69
+ TTL: '900',
70
+ Type: 'CNAME'
71
+ }
72
+ }
73
+ end
74
+
75
+ resource 'LoadBalancer' do
76
+ {
77
+ Type: 'AWS::ElasticLoadBalancing::LoadBalancer',
78
+ Properties: {
79
+ AvailabilityZones: [fn.select(0, fn.get_azs)],
80
+ Listeners: [
81
+ {
82
+ InstancePort: 5432,
83
+ Protocol: 'TCP'
84
+ }
85
+ ],
86
+ Instances: [ref('PrimaryInstance')]
87
+ }
88
+ }
89
+ end
90
+ end
@@ -0,0 +1,47 @@
1
+ Cumuliform.template do
2
+ # See http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/parameters-section-structure.html
3
+ parameter 'AMI' do
4
+ {
5
+ Description: 'The AMI id for our template (defaults to the stock Ubuntu 14.04 image in eu-central-1)',
6
+ Type: 'String',
7
+ Default: 'ami-accff2b1'
8
+ }
9
+ end
10
+
11
+ # See http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/mappings-section-structure.html
12
+ mapping 'RegionAMI' do
13
+ {
14
+ 'eu-central-1' => {
15
+ 'hvm' => 'ami-accff2b1',
16
+ 'pv' => 'ami-b6cff2ab'
17
+ },
18
+ 'eu-west-1' => {
19
+ 'hvm' => 'ami-47a23a30',
20
+ 'pv' => 'ami-5da23a2a'
21
+ }
22
+ }
23
+ end
24
+
25
+ # See http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/conditions-section-structure.html
26
+ condition 'Ireland' do
27
+ fn.equals(ref('AWS::Region'), 'eu-west-1')
28
+ end
29
+
30
+ # See http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/resources-section-structure.html
31
+ resource 'PrimaryInstance' do
32
+ {
33
+ Type: 'AWS::EC2::Instance',
34
+ Properties: {
35
+ ImageId: ref('AMI'),
36
+ InstanceType: 'm3.medium'
37
+ }
38
+ }
39
+ end
40
+
41
+ # See http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/outputs-section-structure.html
42
+ output 'PrimaryInstanceID' do
43
+ {
44
+ Value: ref('PrimaryInstance')
45
+ }
46
+ end
47
+ end
data/exe/cumuliform CHANGED
File without changes
@@ -2,18 +2,39 @@ require_relative 'error'
2
2
 
3
3
  module Cumuliform
4
4
  module Fragments
5
- def fragments
6
- @fragments ||= {}
5
+ # Define a fragment for later use.
6
+ #
7
+ # Essentially stores a block under the name given for later use.
8
+ #
9
+ # @param name [Symbol] name of the fragment to define
10
+ # @yieldparam opts [Hash] will yield the options hash passed to
11
+ # <tt>#fragment()</tt> when called
12
+ # @raise [Error::FragmentAlreadyDefined] if the <tt>name</tt> is not unique
13
+ # in this template
14
+ def def_fragment(name, &block)
15
+ if fragments.has_key?(name)
16
+ raise Error::FragmentAlreadyDefined, name
17
+ end
18
+ fragments[name] = block
7
19
  end
8
20
 
21
+ # Use an already-defined fragment
22
+ #
23
+ # Retrieves the block stored under <tt>name</tt> and calls it, passing any options.
24
+ #
25
+ # @param name [Symbol] The name of the fragment to use
26
+ # @param opts [Hash] Options to be passed to the fragment
27
+ # @return [Object<JSON-serialisable>] the return value of the called block
9
28
  def fragment(name, *args, &block)
10
29
  if block_given?
11
- define_fragment(name, block)
30
+ warn "fragment definition form (with block) is deprecated. Use #def_fragment instead"
31
+ def_fragment(name, *args, &block)
12
32
  else
13
- include_fragment(name, *args)
33
+ use_fragment(name, *args)
14
34
  end
15
35
  end
16
36
 
37
+ # @api private
17
38
  def find_fragment(name)
18
39
  local_fragment = fragments[name]
19
40
  imports.reverse.reduce(local_fragment) { |fragment, import|
@@ -21,24 +42,22 @@ module Cumuliform
21
42
  }
22
43
  end
23
44
 
24
- def has_fragment?(name)
25
- !find_fragment(name).nil?
26
- end
27
-
28
45
  private
29
46
 
30
- def define_fragment(name, block)
31
- if fragments.has_key?(name)
32
- raise Error::FragmentAlreadyDefined, name
33
- end
34
- fragments[name] = block
35
- end
36
-
37
- def include_fragment(name, opts = {})
47
+ def use_fragment(name, opts = {})
38
48
  unless has_fragment?(name)
39
49
  raise Error::FragmentNotFound, name
40
50
  end
41
51
  instance_exec(opts, &find_fragment(name))
42
52
  end
53
+
54
+
55
+ def fragments
56
+ @fragments ||= {}
57
+ end
58
+
59
+ def has_fragment?(name)
60
+ !find_fragment(name).nil?
61
+ end
43
62
  end
44
63
  end
@@ -2,55 +2,199 @@ require_relative 'error'
2
2
 
3
3
  module Cumuliform
4
4
  module Functions
5
+ # implements wrappers for the intrinsic functions Fn::*
5
6
  class IntrinsicFunctions
6
7
  attr_reader :template
7
8
 
9
+ # @api private
8
10
  def initialize(template)
9
11
  @template = template
10
12
  end
11
13
 
14
+ # Wraps Fn::FindInMap
15
+ #
16
+ # see http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-findinmap.html
17
+ #
18
+ # @param mapping_logical_id [String] The logical ID of the mapping we
19
+ # want to look up a value from
20
+ # @param level_1_key [String] Key 1
21
+ # @param level_2_key [String] Key 2
22
+ # @return [Hash] the Fn::FindInMap object
12
23
  def find_in_map(mapping_logical_id, level_1_key, level_2_key)
13
24
  template.verify_mapping_logical_id!(mapping_logical_id)
14
25
  {"Fn::FindInMap" => [mapping_logical_id, level_1_key, level_2_key]}
15
26
  end
16
27
 
28
+ # Wraps Fn::GetAtt
29
+ #
30
+ # see http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-getatt.html
31
+ #
32
+ # @param resource_logical_id [String] The Logical ID of resource we want
33
+ # to get an attribute of
34
+ # @param attr_name [String] The name of the attribute to get the value of
35
+ # @return [Hash] the Fn::GetAtt object
17
36
  def get_att(resource_logical_id, attr_name)
18
37
  template.verify_resource_logical_id!(resource_logical_id)
19
38
  {"Fn::GetAtt" => [resource_logical_id, attr_name]}
20
39
  end
21
40
 
41
+ # Wraps Fn::Join
42
+ #
43
+ # see http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-join.html
44
+ #
45
+ # @param separator [String] The separator string to join the array
46
+ # elements with
47
+ # @param args [Array<String>] The array of strings to join
48
+ # @return [Hash] the Fn::Join object
22
49
  def join(separator, args)
23
50
  raise ArgumentError, "Second argument must be an Array" unless args.is_a?(Array)
24
51
  {"Fn::Join" => [separator, args]}
25
52
  end
26
53
 
54
+ # Wraps Fn::Base64
55
+ #
56
+ # see http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-base64.html
57
+ #
58
+ # The argument should either be a string or an intrinsic function that
59
+ # evaluates to a string when CloudFormation executes the template
60
+ #
61
+ # see http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-base64.html
62
+ #
63
+ # @param value [String, Hash<string-returning instrinsic function>]
64
+ # The separator string to join the array elements with
65
+ # @return [Hash] the Fn::Base64 object
27
66
  def base64(value)
28
67
  {"Fn::Base64" => value}
29
68
  end
30
69
 
70
+ # Wraps Fn::GetAZs
71
+ #
72
+ # CloudFormation evaluates this to an array of availability zone names.
73
+ #
74
+ # see http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-getavailabilityzones.html
75
+ # @param value [String, Hash<ref('AWS::Region')>] The AWS region to get
76
+ # the array of Availability Zones of. Empty string (the default) is
77
+ # equivalent to specifying `ref('AWS::Region')` which evaluates to the
78
+ # region the stack is being created in
31
79
  def get_azs(value = "")
32
80
  {"Fn::GetAZs" => value}
33
81
  end
34
82
 
83
+ # Wraps Fn::Equals
84
+ #
85
+ # see http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-conditions.html#d0e86148
86
+ #
87
+ # The arguments should be the literal values or refs you want to
88
+ # compare. Returns true or false when CloudFormation evaluates the
89
+ # template.
90
+ # @param value [String, Hash<value-returning ref>]
91
+ # @param other_value [String, Hash<value-returning ref>]
92
+ # @return [Hash] the Fn::Equals object
35
93
  def equals(value, other_value)
36
94
  {"Fn::Equals" => [value, other_value]}
37
95
  end
38
96
 
97
+ # Wraps Fn::If
98
+ #
99
+ # see http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-conditions.html#d0e86223
100
+ #
101
+ # CloudFormation evaluates the Condition referred to the logical ID in
102
+ # the <tt>condition</tt> arg and returns the <tt>true_value</tt> if
103
+ # <tt>true</tt> and <tt>false_value</tt> otherwise. <tt>condition</tt>
104
+ # cannot be an <tt>Fn::Ref</tt>, but you can use our <tt>xref()</tt>
105
+ # helper to ensure the logical ID is valid.
106
+ #
107
+ # @param condition[String] the Logical ID of the Condition to be checked
108
+ # @param true_value the value to be returned if <tt>condition</tt>
109
+ # evaluates true
110
+ # @param false_value the value to be returned if <tt>condition</tt>
111
+ # evaluates false
112
+ # @return [Hash] the Fn::If object
39
113
  def if(condition, true_value, false_value)
40
114
  {"Fn::If" => [condition, true_value, false_value]}
41
115
  end
42
116
 
117
+ # Wraps Fn::Select
118
+ #
119
+ # see http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-select.html
120
+ #
121
+ # CloudFormation evaluates the <tt>index</tt> (which can be an
122
+ # integer-as-a-string or a <tt>ref</tt> which evaluates to a number) and
123
+ # returns the corresponding item from the array (which can be an array
124
+ # literal, or the result of <tt>Fn::GetAZs</tt>, or one of
125
+ # <tt>Fn::GetAtt</tt>, <tt>Fn::If</tt>, and <tt>Ref</tt> (if they would
126
+ # return an Array).
127
+ #
128
+ # @param index [Integer, Hash<value-returning ref>] The index to
129
+ # retrieve from <tt>array</tt>
130
+ # @param array [Array, Hash<array-returning ref of intrinsic function>]
131
+ # The array to retrieve from
43
132
  def select(index, array)
44
- unless index.is_a?(Integer) && index >= 0
45
- raise ArgumentError, "index must be a positive integer"
133
+ ref_style_index = index.is_a?(Hash) && index.has_key?("Fn::Ref")
134
+ positive_int_style_index = index.is_a?(Integer) && index >= 0
135
+ unless ref_style_index || positive_int_style_index
136
+ raise ArgumentError, "index must be a positive integer or Fn::Ref"
46
137
  end
47
- if array.is_a?(Array) && index >= array.length
48
- raise IndexError, "index must be in the range 0 <= index < array.length"
138
+ if positive_int_style_index
139
+ if array.is_a?(Array) && index >= array.length
140
+ raise IndexError, "index must be in the range 0 <= index < array.length"
141
+ end
142
+ index = index.to_s
49
143
  end
50
- {"Fn::Select" => [index.to_s, array]}
144
+ {"Fn::Select" => [index, array]}
145
+ end
146
+
147
+ # Wraps Fn::And
148
+ #
149
+ # see http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-conditions.html#d0e86066
150
+ #
151
+ # Behaves as a logical AND operator for CloudFormation conditions. Arguments should be other conditions or things that will evaluate to <tt>true</tt> or <tt>false</tt>.
152
+ #
153
+ # @param condition_1 [Hash<boolean-returning ref, intrinsic function, or condition>] Condition / value to be ANDed
154
+ # @param condition_n [Hash<boolean-returning ref, intrinsic function, or condition>] Condition / value to be ANDed (min 2, max 10 condition args)
155
+ def and(*conditions)
156
+ unless (2..10).cover?(conditions.length)
157
+ raise ArgumentError, "You must specify AT LEAST 2 and AT MOST 10 conditions"
158
+ end
159
+ {"Fn::And" => conditions}
160
+ end
161
+
162
+ # Wraps Fn::Or
163
+ #
164
+ # see http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-conditions.html#d0e86490
165
+ #
166
+ # Behaves as a logical OR operator for CloudFormation conditions. Arguments should be other conditions or things that will evaluate to <tt>true</tt> or <tt>false</tt>.
167
+ #
168
+ # @param condition_1 [Hash<boolean-returning ref, intrinsic function, or condition>] Condition / value to be ORed
169
+ # @param condition_n [Hash<boolean-returning ref, intrinsic function, or condition>] Condition / value to be ORed (min 2, max 10 condition args)
170
+ def or(*conditions)
171
+ unless (2..10).cover?(conditions.length)
172
+ raise ArgumentError, "You must specify AT LEAST 2 and AT MOST 10 conditions"
173
+ end
174
+ {"Fn::Or" => conditions}
175
+ end
176
+
177
+ # Wraps Fn::Not
178
+ #
179
+ # see http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-conditions.html#d0e86402
180
+ #
181
+ # Behaves as a logical NOT operator for CloudFormation conditions. The argument should be another condition or something that will evaluate to <tt>true</tt> or <tt>false</tt>
182
+ # @param condition [Hash<boolean-returning ref, intrinsic function, or condition>] Condition / value to be NOTed
183
+ def not(condition)
184
+ {"Fn::Not" => [condition]}
51
185
  end
52
186
  end
53
187
 
188
+ # Checks <tt>logical_id</tt> is present and either returns <tt>logical_id</tt> or raises
189
+ # Cumuliform::Error::NoSuchLogicalId.
190
+ #
191
+ # You can use it anywhere you need a string Logical ID and want the
192
+ # protection of having it be verified, for example in the <tt>cfn-init</tt>
193
+ # invocation in a Cfn::Init metadata block or the condition name field
194
+ # of, e.g. Fn::And.
195
+ #
196
+ # @param logical_id [String] the logical ID you want to check
197
+ # @return [String] the logical_id param
54
198
  def xref(logical_id)
55
199
  unless has_logical_id?(logical_id)
56
200
  raise Error::NoSuchLogicalId, logical_id
@@ -58,10 +202,17 @@ module Cumuliform
58
202
  logical_id
59
203
  end
60
204
 
205
+ # Wraps Ref
206
+ #
207
+ # CloudFormation evaluates the <tt>Ref</tt> and returns the value of the Parameter or Resource with Logical ID <tt>logical_id</tt>.
208
+ #
209
+ # @param logical_id [String] The logical ID of the parameter or resource
61
210
  def ref(logical_id)
62
211
  {"Ref" => xref(logical_id)}
63
212
  end
64
213
 
214
+ # returns an instance of IntrinsicFunctions which provides wrappers for
215
+ # Fn::* functions
65
216
  def fn
66
217
  @fn ||= IntrinsicFunctions.new(self)
67
218
  end
@@ -1,3 +1,3 @@
1
1
  module Cumuliform
2
- VERSION = "0.4.0"
2
+ VERSION = "0.5.1"
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: cumuliform
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.0
4
+ version: 0.5.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Matt Patterson
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2015-06-10 00:00:00.000000000 Z
11
+ date: 2015-12-03 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -52,8 +52,22 @@ dependencies:
52
52
  - - "~>"
53
53
  - !ruby/object:Gem::Version
54
54
  version: '3'
55
+ - !ruby/object:Gem::Dependency
56
+ name: yard
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0.8'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0.8'
55
69
  description: |
56
- Simple DSL for generation AWS CloudFormation templates with an emphasis
70
+ Simple DSL for generating AWS CloudFormation templates with an emphasis
57
71
  on ensuring you don't shoot yourself in the foot by, e.g. referencing
58
72
  non-existent resources because you have a typo.
59
73
  email:
@@ -66,6 +80,7 @@ files:
66
80
  - ".gitignore"
67
81
  - ".ruby-version"
68
82
  - ".travis.yml"
83
+ - CHANGELOG.md
69
84
  - CODE_OF_CONDUCT.md
70
85
  - Gemfile
71
86
  - LICENSE.txt
@@ -75,7 +90,11 @@ files:
75
90
  - bin/setup
76
91
  - cumuliform.gemspec
77
92
  - examples/Rakefile
93
+ - examples/condition_functions.rb
94
+ - examples/fragments.rb
95
+ - examples/intrinsic_functions.rb
78
96
  - examples/simplest.rb
97
+ - examples/top-level-declarations.rb
79
98
  - exe/cumuliform
80
99
  - lib/cumuliform.rb
81
100
  - lib/cumuliform/error.rb
@@ -111,3 +130,4 @@ signing_key:
111
130
  specification_version: 4
112
131
  summary: DSL library for generating AWS CloudFormation templates
113
132
  test_files: []
133
+ has_rdoc: