cumuliform 0.4.0 → 0.5.1

Sign up to get free protection for your applications and to get access to all the features.
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: