cfoo 0.0.1 → 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +90 -15
- data/TODO +3 -5
- data/bin/cfoo +2 -7
- data/features/error_reporting.feature +23 -0
- data/features/function_expansion.feature +139 -0
- data/features/step_definitions/cfoo_steps.rb +14 -2
- data/lib/cfoo.rb +4 -0
- data/lib/cfoo/array.rb +13 -0
- data/lib/cfoo/cfoo.rb +6 -2
- data/lib/cfoo/constants.rb +3 -0
- data/lib/cfoo/el_parser.rb +35 -20
- data/lib/cfoo/factory.rb +25 -0
- data/lib/cfoo/file_system.rb +20 -0
- data/lib/cfoo/module.rb +0 -4
- data/lib/cfoo/parser.rb +66 -19
- data/lib/cfoo/processor.rb +10 -10
- data/lib/cfoo/version.rb +1 -1
- data/lib/cfoo/yaml_parser.rb +2 -1
- data/spec/cfoo/array_spec.rb +23 -0
- data/spec/cfoo/cfoo_spec.rb +20 -1
- data/spec/cfoo/el_parser_spec.rb +31 -0
- data/spec/cfoo/factory_spec.rb +22 -0
- data/spec/cfoo/file_system_spec.rb +38 -0
- data/spec/cfoo/parser_spec.rb +13 -1
- data/spec/cfoo/processor_spec.rb +111 -3
- data/spec/cfoo/yaml_parser_spec.rb +5 -0
- data/spec/cfoo/yaml_spec.rb +36 -0
- metadata +146 -135
- checksums.yaml +0 -7
data/README.md
CHANGED
@@ -4,7 +4,11 @@
|
|
4
4
|
[![Coverage Status](https://coveralls.io/repos/drrb/cfoo/badge.png?branch=master)](https://coveralls.io/r/drrb/cfoo)
|
5
5
|
[![Code Climate](https://codeclimate.com/github/drrb/cfoo.png)](https://codeclimate.com/github/drrb/cfoo)
|
6
6
|
|
7
|
-
|
7
|
+
[![Gem Version](https://badge.fury.io/rb/cfoo.png)](https://badge.fury.io/rb/cfoo)
|
8
|
+
[![Dependency Status](https://gemnasium.com/drrb/cfoo.png)](https://gemnasium.com/drrb/cfoo)
|
9
|
+
|
10
|
+
Cfoo (pronounced "sifu") lets you write your [CloudFormation](https://aws.amazon.com/cloudformation)
|
11
|
+
templates [in YAML](#templates), and makes it easier with some [helpers](#shortcuts).
|
8
12
|
|
9
13
|
## Installation
|
10
14
|
|
@@ -14,9 +18,17 @@ Cfoo can be installed as a Ruby Gem
|
|
14
18
|
|
15
19
|
## Usage
|
16
20
|
|
17
|
-
|
21
|
+
1. Write your CloudFormation templates using Cfoo YAML
|
18
22
|
|
19
|
-
|
23
|
+
2. Turn your Cfoo templates into normal CloudFormation templates
|
24
|
+
```terminal
|
25
|
+
$ cfoo web-server.template.yml database.template.yml > web-stack.template.json
|
26
|
+
```
|
27
|
+
|
28
|
+
3. Create your stack with CloudFormation
|
29
|
+
```terminal
|
30
|
+
$ cfn-create-stack --stack-name WebStack -f web-stack.template.json
|
31
|
+
```
|
20
32
|
|
21
33
|
## Templates
|
22
34
|
|
@@ -72,7 +84,41 @@ Properties:
|
|
72
84
|
/opt/aws/bin/cfn-signal -e 0 -r "cfn-init completed" '$(WaitHandle)'
|
73
85
|
```
|
74
86
|
|
75
|
-
|
87
|
+
## Projects
|
88
|
+
|
89
|
+
Using Cfoo, it is possible to split your templates up into logical components that will
|
90
|
+
combined to form your CloudFormation template.
|
91
|
+
|
92
|
+
First, create a directory in your project directory called `modules`. For each module,
|
93
|
+
create some Cfoo templates defining the different parts of your app. Your project
|
94
|
+
structure will look like this:
|
95
|
+
|
96
|
+
```
|
97
|
+
my-web-app
|
98
|
+
└── modules
|
99
|
+
├── application
|
100
|
+
│ ├── app_servers.yml
|
101
|
+
│ ├── database.yml
|
102
|
+
│ └── public_load_balancer.yml
|
103
|
+
├── instances
|
104
|
+
│ └── instance_mappings.yml
|
105
|
+
└── network
|
106
|
+
├── bastion.yml
|
107
|
+
├── cfn_user.yml
|
108
|
+
├── dns.yml
|
109
|
+
├── nat.yml
|
110
|
+
├── private_subnet.yml
|
111
|
+
├── public_subnet.yml
|
112
|
+
└── vpc.yml
|
113
|
+
```
|
114
|
+
|
115
|
+
Use Cfoo to generate your project's CloudFormation template
|
116
|
+
|
117
|
+
```terminal
|
118
|
+
$ cfoo > web-app.template.json
|
119
|
+
```
|
120
|
+
|
121
|
+
## Shortcuts
|
76
122
|
|
77
123
|
Cfoo allows you to simplify CloudFormation [intrinsic function](http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference.html)
|
78
124
|
references using its own shorthand
|
@@ -106,31 +152,61 @@ Cfoo Shortcut: `$(HostedZone).`
|
|
106
152
|
### YAML Types
|
107
153
|
|
108
154
|
Cfoo gives you the option of using YAML custom data-types where it helps to make your templates easier to read.
|
109
|
-
The table below uses inline YAML lists, but multiline lists can also be used.
|
110
155
|
|
111
156
|
##### Reference
|
112
157
|
|
113
|
-
CloudFormation:
|
158
|
+
CloudFormation:
|
159
|
+
```json
|
160
|
+
{ "Ref" : "InstanceType" }
|
161
|
+
```
|
114
162
|
|
115
|
-
YAML Type:
|
163
|
+
YAML Type:
|
164
|
+
```yaml
|
165
|
+
!Ref InstanceType
|
166
|
+
```
|
116
167
|
|
117
168
|
##### Mapping Reference
|
118
169
|
|
119
|
-
CloudFormation:
|
170
|
+
CloudFormation:
|
171
|
+
```json
|
172
|
+
{ "FindInMap" : [ "SubnetConfig", "VPC", "CIDR" ] }
|
173
|
+
```
|
120
174
|
|
121
|
-
YAML Type:
|
175
|
+
YAML Type:
|
176
|
+
```yaml
|
177
|
+
!FindInMap [ SubnetConfig, VPC, CIDR ]
|
178
|
+
```
|
122
179
|
|
123
180
|
##### Attribute Reference
|
124
181
|
|
125
|
-
CloudFormation:
|
182
|
+
CloudFormation:
|
183
|
+
```json
|
184
|
+
{ "Fn::GetAtt" : [ "Ec2Instance", "PublicIp" ] }
|
185
|
+
```
|
126
186
|
|
127
|
-
YAML Type:
|
187
|
+
YAML Type:
|
188
|
+
```yaml
|
189
|
+
!GetAtt [ Ec2Instance, PublicIp ]
|
190
|
+
```
|
128
191
|
|
129
192
|
##### Base64 String
|
130
193
|
|
131
|
-
CloudFormation:
|
194
|
+
CloudFormation:
|
195
|
+
```json
|
196
|
+
{ "Fn::Base64" : "#!/bin/bash\necho 'Running script...'" }
|
197
|
+
```
|
198
|
+
|
199
|
+
YAML Type:
|
200
|
+
```yaml
|
201
|
+
!Base64 "#!/bin/bash\necho 'running script...'"
|
202
|
+
```
|
132
203
|
|
133
|
-
YAML Type:
|
204
|
+
Alternative YAML Type:
|
205
|
+
```yaml
|
206
|
+
!Base64 |
|
207
|
+
#!/bin/bash
|
208
|
+
echo 'running script...'
|
209
|
+
```
|
134
210
|
|
135
211
|
## Goals
|
136
212
|
|
@@ -144,9 +220,8 @@ Cfoo aims to let developers simplify CloudFormation templates by:
|
|
144
220
|
|
145
221
|
### Secondary Goals
|
146
222
|
|
147
|
-
Cfoo aims (subject to Primary Goals) to:
|
223
|
+
Cfoo also aims (subject to Primary Goals) to:
|
148
224
|
|
149
|
-
- allow all aspects of CloudFormation templates to be expressed in YAML (so you don't need to use JSON)
|
150
225
|
- allow inclusion existing JSON templates (so you don't have to switch all at once)
|
151
226
|
|
152
227
|
### Non-goals
|
data/TODO
CHANGED
@@ -2,13 +2,12 @@ Features
|
|
2
2
|
----------
|
3
3
|
More tests for EL parsing
|
4
4
|
Spaces in EL?
|
5
|
-
|
5
|
+
Allow lone dollar signs in strings? (escaping is only necessary if the dollar is in front of a bracket)
|
6
6
|
Shorthand for AWS variables (e.g. { "Ref" : "AWS::StackId" } )
|
7
|
-
|
8
|
-
Better errors when parsing fails
|
7
|
+
Allow arrays as function args (e.g. $(Join("", [1,2,3])))
|
9
8
|
Split into resources/mappings/parameters/etc by putting templates into directories with those names
|
10
|
-
Allow directory for raw templates
|
11
9
|
Allow traditional JSON CloudFormation templates (including modules, splitting, etc)
|
10
|
+
Expose parser, for use with Fog (github fog/fog)
|
12
11
|
Vim highlighting
|
13
12
|
Use multi-json instead of json gem (spec.add_dependency 'multi_json', '~> 1.0')
|
14
13
|
Disablable warnings about naming conventions
|
@@ -23,7 +22,6 @@ Tests
|
|
23
22
|
|
24
23
|
Tasks
|
25
24
|
----------
|
26
|
-
Refactor EL parser
|
27
25
|
|
28
26
|
Bugs
|
29
27
|
----------
|
data/bin/cfoo
CHANGED
@@ -11,13 +11,8 @@ end
|
|
11
11
|
|
12
12
|
filenames = ARGV
|
13
13
|
|
14
|
-
|
15
|
-
|
16
|
-
project = Cfoo::Project.new(file_system)
|
17
|
-
parser = Cfoo::Parser.new(file_system)
|
18
|
-
processor = Cfoo::Processor.new(parser, project)
|
19
|
-
renderer = Cfoo::Renderer.new
|
20
|
-
cfoo = Cfoo::Cfoo.new(processor, renderer, STDOUT)
|
14
|
+
factory = Cfoo::Factory.new(STDOUT, STDERR)
|
15
|
+
cfoo = factory.cfoo
|
21
16
|
|
22
17
|
if filenames.empty?
|
23
18
|
cfoo.build_project
|
@@ -0,0 +1,23 @@
|
|
1
|
+
Feature: Error reporting
|
2
|
+
As a Cfoo user
|
3
|
+
I want to get useful parse errors
|
4
|
+
So that I can debug my templates
|
5
|
+
|
6
|
+
Scenario: YAML Parse error
|
7
|
+
Given I have a file "bad_yaml.yml" containing
|
8
|
+
"""
|
9
|
+
EntryPoint: Key: Value:
|
10
|
+
"""
|
11
|
+
When I process "bad_yaml.yml"
|
12
|
+
Then I should see an error containing "bad_yaml.yml"
|
13
|
+
And I should see an error containing "line"
|
14
|
+
And I should see an error containing "col"
|
15
|
+
|
16
|
+
Scenario: EL Parse error
|
17
|
+
Given I have a file "bad_el.yml" containing
|
18
|
+
"""
|
19
|
+
EntryPoint: $(
|
20
|
+
"""
|
21
|
+
When I process "bad_el.yml"
|
22
|
+
Then I should see an error containing "Source: $("
|
23
|
+
And I should see an error containing "Location: bad_el.yml line 1, column 13"
|
@@ -0,0 +1,139 @@
|
|
1
|
+
Feature: Expand Function Calls
|
2
|
+
As a CloudFormation user
|
3
|
+
I want to use an expression language as a shorthand for function calls
|
4
|
+
So that I my templates are easier to read
|
5
|
+
|
6
|
+
Scenario: Function with no parameters
|
7
|
+
Given I have a file "autoscaling_group.yml" containing
|
8
|
+
"""
|
9
|
+
WebServerGroup:
|
10
|
+
Type: AWS::AutoScaling::AutoScalingGroup
|
11
|
+
Properties:
|
12
|
+
AvailabilityZones: $(GetAZs())
|
13
|
+
"""
|
14
|
+
When I process "autoscaling_group.yml"
|
15
|
+
Then the output should match JSON
|
16
|
+
"""
|
17
|
+
{
|
18
|
+
"AWSTemplateFormatVersion" : "2010-09-09",
|
19
|
+
"WebServerGroup" : {
|
20
|
+
"Type" : "AWS::AutoScaling::AutoScalingGroup",
|
21
|
+
"Properties" : {
|
22
|
+
"AvailabilityZones" : { "Fn::GetAZs" : "" }
|
23
|
+
}
|
24
|
+
}
|
25
|
+
}
|
26
|
+
"""
|
27
|
+
|
28
|
+
Scenario: Function with one parameter
|
29
|
+
Given I have a file "autoscaling_group.yml" containing
|
30
|
+
"""
|
31
|
+
WebServerGroup:
|
32
|
+
Type: AWS::AutoScaling::AutoScalingGroup
|
33
|
+
Properties:
|
34
|
+
AvailabilityZones: $(GetAZs(us-east-1))
|
35
|
+
"""
|
36
|
+
When I process "autoscaling_group.yml"
|
37
|
+
Then the output should match JSON
|
38
|
+
"""
|
39
|
+
{
|
40
|
+
"AWSTemplateFormatVersion" : "2010-09-09",
|
41
|
+
"WebServerGroup" : {
|
42
|
+
"Type" : "AWS::AutoScaling::AutoScalingGroup",
|
43
|
+
"Properties" : {
|
44
|
+
"AvailabilityZones" : { "Fn::GetAZs" : "us-east-1" }
|
45
|
+
}
|
46
|
+
}
|
47
|
+
}
|
48
|
+
"""
|
49
|
+
|
50
|
+
Scenario: Function with multiple parameters
|
51
|
+
Given I have a file "autoscaling_group.yml" containing
|
52
|
+
"""
|
53
|
+
FrontendFleet:
|
54
|
+
Type: AWS::AutoScaling::AutoScalingGroup
|
55
|
+
Properties:
|
56
|
+
AvailabilityZones:
|
57
|
+
- $(GetAtt(PrivateSubnet, AvailabilityZone))
|
58
|
+
"""
|
59
|
+
When I process "autoscaling_group.yml"
|
60
|
+
Then the output should match JSON
|
61
|
+
"""
|
62
|
+
{
|
63
|
+
"AWSTemplateFormatVersion" : "2010-09-09",
|
64
|
+
"FrontendFleet" : {
|
65
|
+
"Type" : "AWS::AutoScaling::AutoScalingGroup",
|
66
|
+
"Properties" : {
|
67
|
+
"AvailabilityZones" : [{ "Fn::GetAtt" : [ "PrivateSubnet", "AvailabilityZone" ] }]
|
68
|
+
}
|
69
|
+
}
|
70
|
+
}
|
71
|
+
"""
|
72
|
+
|
73
|
+
Scenario: Function with no spaces between arguments
|
74
|
+
Given I have a file "autoscaling_group.yml" containing
|
75
|
+
"""
|
76
|
+
FrontendFleet:
|
77
|
+
Type: AWS::AutoScaling::AutoScalingGroup
|
78
|
+
Properties:
|
79
|
+
AvailabilityZones:
|
80
|
+
- $(GetAtt(PrivateSubnet,AvailabilityZone))
|
81
|
+
"""
|
82
|
+
When I process "autoscaling_group.yml"
|
83
|
+
Then the output should match JSON
|
84
|
+
"""
|
85
|
+
{
|
86
|
+
"AWSTemplateFormatVersion" : "2010-09-09",
|
87
|
+
"FrontendFleet" : {
|
88
|
+
"Type" : "AWS::AutoScaling::AutoScalingGroup",
|
89
|
+
"Properties" : {
|
90
|
+
"AvailabilityZones" : [{ "Fn::GetAtt" : [ "PrivateSubnet", "AvailabilityZone" ] }]
|
91
|
+
}
|
92
|
+
}
|
93
|
+
}
|
94
|
+
"""
|
95
|
+
|
96
|
+
Scenario: Function with lots of spaces between and around arguments
|
97
|
+
Given I have a file "autoscaling_group.yml" containing
|
98
|
+
"""
|
99
|
+
FrontendFleet:
|
100
|
+
Type: AWS::AutoScaling::AutoScalingGroup
|
101
|
+
Properties:
|
102
|
+
AvailabilityZones:
|
103
|
+
- $(GetAtt( PrivateSubnet , AvailabilityZone ))
|
104
|
+
"""
|
105
|
+
When I process "autoscaling_group.yml"
|
106
|
+
Then the output should match JSON
|
107
|
+
"""
|
108
|
+
{
|
109
|
+
"AWSTemplateFormatVersion" : "2010-09-09",
|
110
|
+
"FrontendFleet" : {
|
111
|
+
"Type" : "AWS::AutoScaling::AutoScalingGroup",
|
112
|
+
"Properties" : {
|
113
|
+
"AvailabilityZones" : [{ "Fn::GetAtt" : [ "PrivateSubnet", "AvailabilityZone" ] }]
|
114
|
+
}
|
115
|
+
}
|
116
|
+
}
|
117
|
+
"""
|
118
|
+
|
119
|
+
Scenario: Function with embedded EL
|
120
|
+
Given I have a file "app_servers.yml" containing
|
121
|
+
"""
|
122
|
+
AppServerLaunchConfig:
|
123
|
+
Type: AWS::AutoScaling::LaunchConfiguration
|
124
|
+
Properties:
|
125
|
+
ImageId: $(FindInMap(AWSRegionArch2AMI, $(AWS::Region), $(AWSInstanceType2Arch[$(FrontendInstanceType)][Arch])))
|
126
|
+
"""
|
127
|
+
When I process "app_servers.yml"
|
128
|
+
Then the output should match JSON
|
129
|
+
"""
|
130
|
+
{
|
131
|
+
"AWSTemplateFormatVersion" : "2010-09-09",
|
132
|
+
"AppServerLaunchConfig" : {
|
133
|
+
"Type": "AWS::AutoScaling::LaunchConfiguration",
|
134
|
+
"Properties" : {
|
135
|
+
"ImageId": { "Fn::FindInMap" : [ "AWSRegionArch2AMI", { "Ref" : "AWS::Region" }, { "Fn::FindInMap" : [ "AWSInstanceType2Arch", { "Ref" : "FrontendInstanceType" }, "Arch" ] } ] }
|
136
|
+
}
|
137
|
+
}
|
138
|
+
}
|
139
|
+
"""
|
@@ -26,7 +26,7 @@ When /^I build the project$/ do
|
|
26
26
|
cfoo.build_project
|
27
27
|
end
|
28
28
|
|
29
|
-
Then
|
29
|
+
Then /^the output should match JSON$/ do |expected_json|
|
30
30
|
begin
|
31
31
|
expected = JSON.parse(expected_json)
|
32
32
|
rescue StandardError => e
|
@@ -42,6 +42,14 @@ Then(/^the output should match JSON$/) do |expected_json|
|
|
42
42
|
actual.should == expected
|
43
43
|
end
|
44
44
|
|
45
|
+
Then /^I should see "(.*?)"$/ do |expected_output|
|
46
|
+
stdout.messages.join("\n").should include expected_output
|
47
|
+
end
|
48
|
+
|
49
|
+
Then /^I should see an error containing "(.*?)"$/ do |expected_output|
|
50
|
+
stderr.messages.join("\n").should include expected_output
|
51
|
+
end
|
52
|
+
|
45
53
|
def write_file(filename, content)
|
46
54
|
file_fqn = resolve_file(filename)
|
47
55
|
mkdir_p(File.dirname file_fqn)
|
@@ -59,7 +67,7 @@ def resolve_file(filename)
|
|
59
67
|
end
|
60
68
|
|
61
69
|
def cfoo
|
62
|
-
@cfoo ||= Cfoo::Cfoo.new(processor, renderer, stdout)
|
70
|
+
@cfoo ||= Cfoo::Cfoo.new(processor, renderer, stdout, stderr)
|
63
71
|
end
|
64
72
|
|
65
73
|
def processor
|
@@ -92,6 +100,10 @@ def stdout
|
|
92
100
|
@output ||= Output.new
|
93
101
|
end
|
94
102
|
|
103
|
+
def stderr
|
104
|
+
@output ||= Output.new
|
105
|
+
end
|
106
|
+
|
95
107
|
After do
|
96
108
|
rm_rf project_root if File.directory? project_root
|
97
109
|
end
|
data/lib/cfoo.rb
CHANGED
@@ -1,5 +1,8 @@
|
|
1
|
+
require "cfoo/array"
|
1
2
|
require "cfoo/cfoo"
|
3
|
+
require "cfoo/constants"
|
2
4
|
require "cfoo/el_parser"
|
5
|
+
require "cfoo/factory"
|
3
6
|
require "cfoo/file_system"
|
4
7
|
require "cfoo/module"
|
5
8
|
require "cfoo/parser"
|
@@ -8,4 +11,5 @@ require "cfoo/project"
|
|
8
11
|
require "cfoo/renderer"
|
9
12
|
require "cfoo/version"
|
10
13
|
require "cfoo/yaml"
|
14
|
+
require "cfoo/yaml_parser"
|
11
15
|
|
data/lib/cfoo/array.rb
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
class Array
|
2
|
+
def join_adjacent_strings
|
3
|
+
return clone if empty?
|
4
|
+
self[1..-1].inject([first]) do |combined_parts, part|
|
5
|
+
previous = combined_parts.pop
|
6
|
+
if previous.class == String && part.class == String
|
7
|
+
combined_parts << previous + part
|
8
|
+
else
|
9
|
+
combined_parts << previous << part
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
data/lib/cfoo/cfoo.rb
CHANGED
@@ -1,15 +1,19 @@
|
|
1
1
|
module Cfoo
|
2
2
|
class Cfoo
|
3
|
-
def initialize(processor, renderer, stdout)
|
4
|
-
@processor, @renderer, @stdout = processor, renderer, stdout
|
3
|
+
def initialize(processor, renderer, stdout, stderr)
|
4
|
+
@processor, @renderer, @stdout, @stderr = processor, renderer, stdout, stderr
|
5
5
|
end
|
6
6
|
|
7
7
|
def process(*filenames)
|
8
8
|
@stdout.puts(@renderer.render @processor.process(*filenames))
|
9
|
+
rescue Exception => error
|
10
|
+
@stderr.puts error
|
9
11
|
end
|
10
12
|
|
11
13
|
def build_project
|
12
14
|
@stdout.puts(@renderer.render @processor.process_all)
|
15
|
+
rescue Exception => error
|
16
|
+
@stderr.puts error
|
13
17
|
end
|
14
18
|
end
|
15
19
|
end
|