cumuliform 0.5.1 → 0.5.2

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.
@@ -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