cumuliform 0.5.1 → 0.5.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,38 @@
1
+ FragmentBaseTemplate = Cumuliform.template do
2
+ def_fragment(:ami_param) do |opts|
3
+ parameter 'AMI' do
4
+ {
5
+ Description: 'AMI id',
6
+ Type: 'String',
7
+ Default: opts[:ami_id]
8
+ }
9
+ end
10
+ end
11
+
12
+ def_fragment(:instance_type) do |opts|
13
+ parameter 'InstanceType' do
14
+ {
15
+ Description: 'InstanceType',
16
+ Type: 'String',
17
+ Default: opts[:type],
18
+ AllowedValues: ['t2.small', 't2.medium', 't2.large']
19
+ }
20
+ end
21
+ end
22
+
23
+ def_fragment(:instance) do |opts|
24
+ resource 'MyInstance' do
25
+ {
26
+ Type: 'AWS::EC2::Instance',
27
+ Properties: {
28
+ ImageId: ref('AMI'),
29
+ InstanceType: ref('InstanceType')
30
+ }
31
+ }
32
+ end
33
+ end
34
+
35
+ fragment(:ami_param, ami_id: 'ami-accff2b1')
36
+ fragment(:instance_type, type: 'm4.medium')
37
+ fragment(:instance)
38
+ end
@@ -0,0 +1,18 @@
1
+ require_relative './import-fragments-base.rb'
2
+
3
+ Cumuliform.template do
4
+ import FragmentBaseTemplate
5
+
6
+ def_fragment(:instance_type) do |opts|
7
+ parameter 'InstanceType' do
8
+ {
9
+ Description: 'InstanceType',
10
+ Type: 'String',
11
+ Default: opts[:type],
12
+ AllowedValues: ['m3.medium', 'm4.large', 'm4.xlarge']
13
+ }
14
+ end
15
+ end
16
+
17
+ fragment(:instance_type, type: 'm3.medium')
18
+ end
@@ -0,0 +1,13 @@
1
+ require_relative './import-base.rb'
2
+
3
+ Cumuliform.template do
4
+ import BaseTemplate
5
+
6
+ parameter 'AMI' do
7
+ {
8
+ Description: 'A different AMI',
9
+ Type: 'String',
10
+ Default: 'ami-DIFFERENT'
11
+ }
12
+ end
13
+ end
@@ -0,0 +1,19 @@
1
+ module AmiHelper
2
+ def ami
3
+ 'ami-accff2b1'
4
+ end
5
+ end
6
+
7
+ Cumuliform.template do
8
+ helpers AmiHelper
9
+
10
+ resource 'MyInstance' do
11
+ {
12
+ Type: 'AWS::EC2::Instance',
13
+ Properties: {
14
+ ImageId: ami,
15
+ InstanceType: 'm3.medium'
16
+ }
17
+ }
18
+ end
19
+ end
data/lib/cumuliform.rb CHANGED
@@ -1,96 +1,4 @@
1
1
  # Simple AWS CloudFormation. Doesn't try to run the commands, doesn't try to
2
2
  # DSLize every last thing. Simple is the watch word
3
3
 
4
- require 'json'
5
- require 'set'
6
- require_relative 'cumuliform/error'
7
- require_relative 'cumuliform/fragments'
8
- require_relative 'cumuliform/functions'
9
- require_relative 'cumuliform/import'
10
- require_relative 'cumuliform/output'
11
-
12
- module Cumuliform
13
- AWS_PSEUDO_PARAMS = %w{
14
- AWS::AccountId AWS::NotificationARNs AWS::NoValue
15
- AWS::Region AWS::StackId AWS::StackName
16
- }
17
- SECTIONS = {
18
- "Parameters" => :parameter,
19
- "Mappings" => :mapping,
20
- "Conditions" => :condition,
21
- "Resources" => :resource,
22
- "Outputs" => :output
23
- }
24
-
25
- def self.template(&block)
26
- template = Template.new
27
- template.define(&block)
28
- end
29
-
30
- class Template
31
- include Import
32
- include Fragments
33
- include Functions
34
- include Output
35
-
36
- def initialize
37
- SECTIONS.each do |section_name, _|
38
- instance_variable_set(:"@#{section_name}", Section.new(section_name, imports))
39
- end
40
- end
41
-
42
- def helpers(*mods, &block)
43
- if block_given?
44
- mods << Module.new(&block)
45
- end
46
- mods.each do |mod|
47
- self.class.include mod
48
- end
49
- end
50
-
51
- def define(&block)
52
- instance_exec(&block)
53
- self
54
- end
55
-
56
- def logical_ids
57
- @logical_ids ||= Set.new(AWS_PSEUDO_PARAMS)
58
- end
59
-
60
- def get_section(name)
61
- instance_variable_get(:"@#{name}")
62
- end
63
-
64
- SECTIONS.each do |section_name, method_name|
65
- error_class = Class.new(Error::IDError) do
66
- def to_s
67
- "No logical ID '#{id}' in section"
68
- end
69
- end
70
- Error.const_set("NoSuchLogicalIdIn#{section_name}", error_class)
71
-
72
- define_method method_name, ->(logical_id, &block) {
73
- add_to_section(section_name, logical_id, block)
74
- }
75
-
76
- define_method :"verify_#{method_name}_logical_id!", ->(logical_id) {
77
- raise error_class, logical_id unless get_section(section_name).member?(logical_id)
78
- true
79
- }
80
- end
81
-
82
- private
83
-
84
- def has_local_logical_id?(logical_id)
85
- logical_ids.include?(logical_id)
86
- end
87
-
88
- def add_to_section(section_name, logical_id, block)
89
- if has_local_logical_id?(logical_id)
90
- raise Error::DuplicateLogicalID, logical_id
91
- end
92
- logical_ids << logical_id
93
- get_section(section_name)[logical_id] = block
94
- end
95
- end
96
- end
4
+ require_relative 'cumuliform/dsl'
@@ -0,0 +1,32 @@
1
+ require_relative 'template'
2
+ require_relative 'dsl/fragments'
3
+ require_relative 'dsl/functions'
4
+ require_relative 'dsl/import'
5
+ require_relative 'dsl/helpers'
6
+
7
+ # Cumuliform is a simple tool for generating AWS CloudFormation templates. It's
8
+ # concerned with making it easier and less error-prone to write templates by
9
+ # providing features that verify references at template generation time, rather
10
+ # than waiting for failures in stack creation. It also allows you to define and
11
+ # reuse template fragments within one template, and between many templates.
12
+ module Cumuliform
13
+ # Create a new template from the passed-in block
14
+ #
15
+ # @param block the template body
16
+ # @return [Template]
17
+ def self.template(&block)
18
+ template = Template.new
19
+ template.define(&block)
20
+ end
21
+
22
+ # The DSL modules contain all of the public DSL methods you'll use in your templates
23
+ module DSL
24
+ end
25
+
26
+ class Template
27
+ include DSL::Import
28
+ include DSL::Fragments
29
+ include DSL::Functions
30
+ include DSL::Helpers
31
+ end
32
+ end
@@ -0,0 +1,67 @@
1
+ require_relative '../error'
2
+
3
+ module Cumuliform
4
+ module DSL
5
+ # DSL methods for creating and reusing template fragments
6
+ module Fragments
7
+ # Define a fragment for later use.
8
+ #
9
+ # Essentially stores a block under the name given for later use.
10
+ #
11
+ # @param name [Symbol] name of the fragment to define
12
+ # @yieldparam opts [Hash] will yield the options hash passed to
13
+ # <tt>#fragment()</tt> when called
14
+ # @raise [Error::FragmentAlreadyDefined] if the <tt>name</tt> is not
15
+ # unique in this template
16
+ def def_fragment(name, &block)
17
+ if fragments.has_key?(name)
18
+ raise Error::FragmentAlreadyDefined, name
19
+ end
20
+ fragments[name] = block
21
+ end
22
+
23
+ # Use an already-defined fragment
24
+ #
25
+ # Retrieves the block stored under <tt>name</tt> and calls it, passing
26
+ # any options.
27
+ #
28
+ # @param name [Symbol] The name of the fragment to use
29
+ # @param opts [Hash] Options to be passed to the fragment
30
+ # @return [Object<JSON-serialisable>] the return value of the called
31
+ # block
32
+ def fragment(name, *args, &block)
33
+ if block_given?
34
+ warn "fragment definition form (with block) is deprecated. Use #def_fragment instead"
35
+ def_fragment(name, *args, &block)
36
+ else
37
+ use_fragment(name, *args)
38
+ end
39
+ end
40
+
41
+ # @api private
42
+ def find_fragment(name)
43
+ local_fragment = fragments[name]
44
+ imports.reverse.reduce(local_fragment) { |fragment, import|
45
+ fragment || import.find_fragment(name)
46
+ }
47
+ end
48
+
49
+ private
50
+
51
+ def use_fragment(name, opts = {})
52
+ unless has_fragment?(name)
53
+ raise Error::FragmentNotFound, name
54
+ end
55
+ instance_exec(opts, &find_fragment(name))
56
+ end
57
+
58
+ def fragments
59
+ @fragments ||= {}
60
+ end
61
+
62
+ def has_fragment?(name)
63
+ !find_fragment(name).nil?
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,256 @@
1
+ require_relative '../error'
2
+
3
+ module Cumuliform
4
+ module DSL
5
+ # DSL methods for working with CloudFormation Intrinsic and Ref functions
6
+ module Functions
7
+ # implements wrappers for the intrinsic functions Fn::*
8
+ class IntrinsicFunctions
9
+ # @api private
10
+ attr_reader :template
11
+
12
+ # @api private
13
+ def initialize(template)
14
+ @template = template
15
+ end
16
+
17
+ # Wraps Fn::FindInMap
18
+ #
19
+ # see http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-findinmap.html
20
+ #
21
+ # @param mapping_logical_id [String] The logical ID of the mapping we
22
+ # want to look up a value from
23
+ # @param level_1_key [String] Key 1
24
+ # @param level_2_key [String] Key 2
25
+ # @return [Hash] the Fn::FindInMap object
26
+ def find_in_map(mapping_logical_id, level_1_key, level_2_key)
27
+ template.verify_mapping_logical_id!(mapping_logical_id)
28
+ {"Fn::FindInMap" => [mapping_logical_id, level_1_key, level_2_key]}
29
+ end
30
+
31
+ # Wraps Fn::GetAtt
32
+ #
33
+ # see http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-getatt.html
34
+ #
35
+ # @param resource_logical_id [String] The Logical ID of resource we
36
+ # want to get an attribute of
37
+ # @param attr_name [String] The name of the attribute to get the value
38
+ # of
39
+ # @return [Hash] the Fn::GetAtt object
40
+ def get_att(resource_logical_id, attr_name)
41
+ template.verify_resource_logical_id!(resource_logical_id)
42
+ {"Fn::GetAtt" => [resource_logical_id, attr_name]}
43
+ end
44
+
45
+ # Wraps Fn::Join
46
+ #
47
+ # see http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-join.html
48
+ #
49
+ # @param separator [String] The separator string to join the array
50
+ # elements with
51
+ # @param args [Array<String>] The array of strings to join
52
+ # @return [Hash] the Fn::Join object
53
+ def join(separator, args)
54
+ raise ArgumentError, "Second argument must be an Array" unless args.is_a?(Array)
55
+ {"Fn::Join" => [separator, args]}
56
+ end
57
+
58
+ # Wraps Fn::Base64
59
+ #
60
+ # see http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-base64.html
61
+ #
62
+ # The argument should either be a string or an intrinsic function that
63
+ # evaluates to a string when CloudFormation executes the template
64
+ #
65
+ # see http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-base64.html
66
+ #
67
+ # @param value [String, Hash<string-returning instrinsic function>] The
68
+ # separator string to join the array elements with
69
+ # @return [Hash] the Fn::Base64 object
70
+ def base64(value)
71
+ {"Fn::Base64" => value}
72
+ end
73
+
74
+ # Wraps Fn::GetAZs
75
+ #
76
+ # CloudFormation evaluates this to an array of availability zone names.
77
+ #
78
+ # see http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-getavailabilityzones.html
79
+ #
80
+ # @param value [String, Hash<ref('AWS::Region')>] The AWS region to get
81
+ # the array of Availability Zones of. Empty string (the default) is
82
+ # equivalent to specifying `ref('AWS::Region')` which evaluates to
83
+ # the region the stack is being created in
84
+ # @return [Hash] the Fn::GetAZs object
85
+ def get_azs(value = "")
86
+ {"Fn::GetAZs" => value}
87
+ end
88
+
89
+ # Wraps Fn::Equals
90
+ #
91
+ # see http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-conditions.html#d0e86148
92
+ #
93
+ # The arguments should be the literal values or refs you want to
94
+ # compare. Returns true or false when CloudFormation evaluates the
95
+ # template.
96
+ #
97
+ # @param value [String, Hash<value-returning ref>]
98
+ # @param other_value [String, Hash<value-returning ref>]
99
+ # @return [Hash] the Fn::Equals object
100
+ def equals(value, other_value)
101
+ {"Fn::Equals" => [value, other_value]}
102
+ end
103
+
104
+ # Wraps Fn::If
105
+ #
106
+ # see http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-conditions.html#d0e86223
107
+ #
108
+ # CloudFormation evaluates the Condition referred to the logical ID in
109
+ # the <tt>condition</tt> arg and returns the <tt>true_value</tt> if
110
+ # <tt>true</tt> and <tt>false_value</tt> otherwise. <tt>condition</tt>
111
+ # cannot be an <tt>Fn::Ref</tt>, but you can use our <tt>xref()</tt>
112
+ # helper to ensure the logical ID is valid.
113
+ #
114
+ # @param condition[String] the Logical ID of the Condition to be
115
+ # checked
116
+ # @param true_value the value to be returned if <tt>condition</tt>
117
+ # evaluates true
118
+ # @param false_value the value to be returned if <tt>condition</tt>
119
+ # evaluates false
120
+ # @return [Hash] the Fn::If object
121
+ def if(condition, true_value, false_value)
122
+ {"Fn::If" => [condition, true_value, false_value]}
123
+ end
124
+
125
+ # Wraps Fn::Select
126
+ #
127
+ # see
128
+ # http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-select.html
129
+ #
130
+ # CloudFormation evaluates the <tt>index</tt> (which can be an
131
+ # integer-as-a-string or a <tt>ref</tt> which evaluates to a number)
132
+ # and returns the corresponding item from the array (which can be an
133
+ # array literal, or the result of <tt>Fn::GetAZs</tt>, or one of
134
+ # <tt>Fn::GetAtt</tt>, <tt>Fn::If</tt>, and <tt>Ref</tt> (if they would
135
+ # return an Array).
136
+ #
137
+ # @param index [Integer, Hash<value-returning ref>] The index to
138
+ # retrieve from <tt>array</tt>
139
+ # @param array [Array, Hash<array-returning ref of intrinsic function>]
140
+ # The array to retrieve from
141
+ # @return [Hash] the Fn::Select object
142
+ def select(index, array)
143
+ ref_style_index = index.is_a?(Hash) && index.has_key?("Fn::Ref")
144
+ positive_int_style_index = index.is_a?(Integer) && index >= 0
145
+ unless ref_style_index || positive_int_style_index
146
+ raise ArgumentError, "index must be a positive integer or Fn::Ref"
147
+ end
148
+ if positive_int_style_index
149
+ if array.is_a?(Array) && index >= array.length
150
+ raise IndexError, "index must be in the range 0 <= index < array.length"
151
+ end
152
+ index = index.to_s
153
+ end
154
+ {"Fn::Select" => [index, array]}
155
+ end
156
+
157
+ # Wraps Fn::And
158
+ #
159
+ # see
160
+ # http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-conditions.html#d0e86066
161
+ #
162
+ # Behaves as a logical AND operator for CloudFormation conditions.
163
+ # Arguments should be other conditions or things that will evaluate to
164
+ # <tt>true</tt> or <tt>false</tt>.
165
+ #
166
+ # @overload and(condition_1, ..., condition_n)
167
+ # @param condition_1 [Hash<boolean-returning ref, intrinsic function,
168
+ # or condition>] Condition / value to be ANDed
169
+ # @param condition_n [Hash<boolean-returning ref, intrinsic function,
170
+ # or condition>] Condition / value to be ANDed (min 2, max 10
171
+ # condition args)
172
+ # @return [Hash] the Fn::And object
173
+ def and(*conditions)
174
+ unless (2..10).cover?(conditions.length)
175
+ raise ArgumentError, "You must specify AT LEAST 2 and AT MOST 10 conditions"
176
+ end
177
+ {"Fn::And" => conditions}
178
+ end
179
+
180
+ # Wraps Fn::Or
181
+ #
182
+ # see
183
+ # http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-conditions.html#d0e86490
184
+ #
185
+ # Behaves as a logical OR operator for CloudFormation conditions.
186
+ # Arguments should be other conditions or things that will evaluate to
187
+ # <tt>true</tt> or <tt>false</tt>.
188
+ #
189
+ # @overload or(condition_1, ..., condition_n)
190
+ # @param condition_1 [Hash<boolean-returning ref, intrinsic function,
191
+ # or condition>] Condition / value to be ORed
192
+ # @param condition_n [Hash<boolean-returning ref, intrinsic function,
193
+ # or condition>] Condition / value to be ORed (min 2, max 10
194
+ # condition args)
195
+ # @return [Hash] the Fn::Or object
196
+ def or(*conditions)
197
+ unless (2..10).cover?(conditions.length)
198
+ raise ArgumentError, "You must specify AT LEAST 2 and AT MOST 10 conditions"
199
+ end
200
+ {"Fn::Or" => conditions}
201
+ end
202
+
203
+ # Wraps Fn::Not
204
+ #
205
+ # see
206
+ # http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-conditions.html#d0e86402
207
+ #
208
+ # Behaves as a logical NOT operator for CloudFormation conditions. The
209
+ # argument should be another condition or something that will evaluate
210
+ # to <tt>true</tt> or <tt>false</tt>
211
+ #
212
+ # @param condition [Hash<boolean-returning ref, intrinsic function, or
213
+ # condition>] Condition / value to be NOTed
214
+ # @return [Hash] the Fn::Not object
215
+ def not(condition)
216
+ {"Fn::Not" => [condition]}
217
+ end
218
+ end
219
+
220
+ # Checks <tt>logical_id</tt> is present and either returns
221
+ # <tt>logical_id</tt> or raises Cumuliform::Error::NoSuchLogicalId.
222
+ #
223
+ # You can use it anywhere you need a string Logical ID and want the
224
+ # protection of having it be verified, for example in the
225
+ # <tt>cfn-init</tt> invocation in a Cfn::Init metadata block or the
226
+ # condition name field of, e.g. Fn::And.
227
+ #
228
+ # @param logical_id [String] the logical ID you want to check
229
+ # @return [String] the logical_id param
230
+ def xref(logical_id)
231
+ unless has_logical_id?(logical_id)
232
+ raise Error::NoSuchLogicalId, logical_id
233
+ end
234
+ logical_id
235
+ end
236
+
237
+ # Wraps Ref
238
+ #
239
+ # CloudFormation evaluates the <tt>Ref</tt> and returns the value of the
240
+ # Parameter or Resource with Logical ID <tt>logical_id</tt>.
241
+ #
242
+ # @param logical_id [String] The logical ID of the parameter or resource
243
+ # @return [Hash] the Ref object
244
+ def ref(logical_id)
245
+ {"Ref" => xref(logical_id)}
246
+ end
247
+
248
+ # returns an instance of IntrinsicFunctions which provides wrappers for
249
+ # Fn::* functions
250
+ # @return [IntrinsicFunctions] the DSL wrapper
251
+ def fn
252
+ @fn ||= IntrinsicFunctions.new(self)
253
+ end
254
+ end
255
+ end
256
+ end