cloudformation-ruby-dsl-addedvars 1.2.4

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,270 @@
1
+ require 'json'
2
+
3
+ ############################ Utility functions
4
+
5
+ # Formats a template as JSON
6
+ def generate_template(template)
7
+ format_json template, !template.nopretty
8
+ end
9
+
10
+ def generate_json(data, pretty = true)
11
+ # Raw formatting
12
+ return JSON.generate(data) unless pretty
13
+
14
+ # Pretty formatting
15
+ JSON.pretty_generate(data)
16
+ end
17
+
18
+
19
+ ############################# Generic DSL
20
+
21
+ class JsonObjectDSL
22
+ def initialize(&block)
23
+ @dict = {}
24
+ instance_eval &block
25
+ end
26
+
27
+ def value(values)
28
+ @dict.update(values)
29
+ end
30
+
31
+ def default(key, value)
32
+ @dict[key] ||= value
33
+ end
34
+
35
+ def to_json(*args)
36
+ @dict.to_json(*args)
37
+ end
38
+
39
+ def print()
40
+ puts JSON.pretty_generate(self)
41
+ end
42
+ end
43
+
44
+ ############################# CloudFormation DSL
45
+
46
+ # Main entry point
47
+ def raw_template(parameters = {}, stack_name = nil, aws_region = default_region, aws_profile = nil, nopretty = false, &block)
48
+ TemplateDSL.new(parameters, stack_name, aws_region, aws_profile, nopretty, &block)
49
+ end
50
+
51
+ def default_region
52
+ ENV['EC2_REGION'] || ENV['AWS_DEFAULT_REGION'] || 'us-east-1'
53
+ end
54
+
55
+ # Core interpreter for the DSL
56
+ class TemplateDSL < JsonObjectDSL
57
+ attr_reader :parameters, :aws_region, :nopretty, :stack_name, :aws_profile
58
+
59
+ def initialize(parameters = {}, stack_name = nil, aws_region = default_region, aws_profile = nil, nopretty = false)
60
+ @parameters = parameters
61
+ @stack_name = stack_name
62
+ @aws_region = aws_region
63
+ @aws_profile = aws_profile
64
+ @nopretty = nopretty
65
+ super()
66
+ end
67
+
68
+ def exec!()
69
+ cfn(self)
70
+ end
71
+
72
+ def parameter(name, options)
73
+ default(:Parameters, {})[name] = options
74
+ @parameters[name] ||= options[:Default]
75
+ end
76
+
77
+ # Find parameters where the specified attribute is true then remove the attribute from the cfn template.
78
+ def excise_parameter_attribute!(attribute)
79
+ marked_parameters = []
80
+ @dict.fetch(:Parameters, {}).each do |param, options|
81
+ if options.delete(attribute.to_sym) or options.delete(attribute.to_s)
82
+ marked_parameters << param
83
+ end
84
+ end
85
+ marked_parameters
86
+ end
87
+
88
+ def mapping(name, options)
89
+ # if options is a string and a valid file then the script will process the external file.
90
+ default(:Mappings, {})[name] = \
91
+ if options.is_a?(Hash); options
92
+ elsif options.is_a?(String); load_from_file(options)['Mappings'][name]
93
+ else; raise("Options for mapping #{name} is neither a string or a hash. Error!")
94
+ end
95
+ end
96
+
97
+ def load_from_file(filename)
98
+ file = File.open(filename)
99
+
100
+ begin
101
+ # Figure out what the file extension is and process accordingly.
102
+ contents = case File.extname(filename)
103
+ when ".rb"; eval(file.read, nil, filename)
104
+ when ".json"; JSON.load(file)
105
+ when ".yaml"; YAML::load(file)
106
+ else; raise("Do not recognize extension of #{filename}.")
107
+ end
108
+ ensure
109
+ file.close
110
+ end
111
+ contents
112
+ end
113
+
114
+ # Find tags where the specified attribute is true then remove this attribute.
115
+ def get_tag_attribute(tags, attribute)
116
+ marked_tags = []
117
+ tags.each do |tag, options|
118
+ if options.delete(attribute.to_sym) or options.delete(attribute.to_s)
119
+ marked_tags << tag
120
+ end
121
+ end
122
+ marked_tags
123
+ end
124
+
125
+ def excise_tags!
126
+ tags = @dict.fetch(:Tags, {})
127
+ @dict.delete(:Tags)
128
+ tags
129
+ end
130
+
131
+ def tag(tag, *args)
132
+ if (tag.is_a?(String) || tag.is_a?(Symbol)) && !args.empty?
133
+ default(:Tags, {})[tag.to_s] = args[0]
134
+ # For backward-compatibility, transform `tag_name=>value` format to `tag_name, :Value=>value, :Immutable=>true`
135
+ # Tags declared this way remain immutable and won't be updated.
136
+ elsif tag.is_a?(Hash) && tag.size == 1 && args.empty?
137
+ $stderr.puts "WARNING: #{tag} tag declaration format is deprecated and will be removed in a future version. Please use resource-like style instead."
138
+ tag.each do |name, value|
139
+ default(:Tags, {})[name.to_s] = {:Value => value, :Immutable => true}
140
+ end
141
+ else
142
+ $stderr.puts "Error: #{tag} tag validation error. Please verify tag's declaration format."
143
+ exit(false)
144
+ end
145
+ end
146
+
147
+ def condition(name, options) default(:Conditions, {})[name] = options end
148
+
149
+ def resource(name, options) default(:Resources, {})[name] = options end
150
+
151
+ def output(name, options) default(:Outputs, {})[name] = options end
152
+
153
+ def find_in_map(map, key, name)
154
+ # Eagerly evaluate mappings when all keys are known at template expansion time
155
+ if map.is_a?(String) && key.is_a?(String) && name.is_a?(String)
156
+ # We don't know whether the map was built with string keys or symbol keys. Try both.
157
+ def get(map, key) map[key] || map.fetch(key.to_sym) end
158
+ get(get(@dict.fetch(:Mappings).fetch(map), key), name)
159
+ else
160
+ { :'Fn::FindInMap' => [ map, key, name ] }
161
+ end
162
+ end
163
+ end
164
+
165
+ def base64(value) { :'Fn::Base64' => value } end
166
+
167
+ def find_in_map(map, key, name) { :'Fn::FindInMap' => [ map, key, name ] } end
168
+
169
+ def get_att(resource, attribute) { :'Fn::GetAtt' => [ resource, attribute ] } end
170
+
171
+ def get_azs(region = '') { :'Fn::GetAZs' => region } end
172
+
173
+ def join(delim, *list)
174
+ case list.length
175
+ when 0 then ''
176
+ else join_list(delim,list)
177
+ end
178
+ end
179
+
180
+ # Variant of join that matches the native CFN syntax.
181
+ def join_list(delim, list) { :'Fn::Join' => [ delim, list ] } end
182
+
183
+ def equal(one, two) { :'Fn::Equals' => [one, two] } end
184
+
185
+ def fn_not(condition) { :'Fn::Not' => [condition] } end
186
+
187
+ def fn_or(*condition_list)
188
+ case condition_list.length
189
+ when 0..1 then raise "fn_or needs at least 2 items."
190
+ when 2..10 then { :'Fn::Or' => condition_list }
191
+ else raise "fn_or needs a list of 2-10 items that evaluate to true/false."
192
+ end
193
+ end
194
+
195
+ def fn_and(*condition_list)
196
+ case condition_list.length
197
+ when 0..1 then raise "fn_and needs at least 2 items."
198
+ when 2..10 then { :'Fn::And' => condition_list }
199
+ else raise "fn_and needs a list of 2-10 items that evaluate to true/false."
200
+ end
201
+ end
202
+
203
+ def fn_if(cond, if_true, if_false) { :'Fn::If' => [cond, if_true, if_false] } end
204
+
205
+ def not_equal(one, two) fn_not(equal(one,two)) end
206
+
207
+ def select(index, list) { :'Fn::Select' => [ index, list ] } end
208
+
209
+ def ref(name) { :Ref => name } end
210
+
211
+ def aws_account_id() ref("AWS::AccountId") end
212
+
213
+ def aws_notification_arns() ref("AWS::NotificationARNs") end
214
+
215
+ def aws_no_value() ref("AWS::NoValue") end
216
+
217
+ def aws_stack_id() ref("AWS::StackId") end
218
+
219
+ def aws_stack_name() ref("AWS::StackName") end
220
+
221
+ # deprecated, for backward compatibility
222
+ def no_value()
223
+ warn_deprecated('no_value()', 'aws_no_value()')
224
+ aws_no_value()
225
+ end
226
+
227
+ # Read the specified file and return its value as a string literal
228
+ def file(filename) File.read(File.absolute_path(filename, File.dirname($PROGRAM_NAME))) end
229
+
230
+ # Interpolates a string like "NAME={{ref('Service')}}" and returns a CloudFormation "Fn::Join"
231
+ # operation to collect the results. Anything between {{ and }} is interpreted as a Ruby expression
232
+ # and eval'd. This is especially useful with Ruby "here" documents.
233
+ # Local variables may also be exposed to the string via the `locals` hash.
234
+ def interpolate(string, locals={})
235
+ list = []
236
+ while string.length > 0
237
+ head, match, string = string.partition(/\{\{.*?\}\}/)
238
+ list << head if head.length > 0
239
+ list << eval(match[2..-3], nil, 'interpolated string') if match.length > 0
240
+ end
241
+
242
+ # Split out strings in an array by newline, for visibility
243
+ list = list.flat_map {|value| value.is_a?(String) ? value.lines.to_a : value }
244
+ join('', *list)
245
+ end
246
+
247
+ def join_interpolate(delim, string)
248
+ $stderr.puts "join_interpolate(delim,string) has been deprecated; use interpolate(string) instead"
249
+ interpolate(string)
250
+ end
251
+
252
+ # This class is used by erb templates so they can access the parameters passed
253
+ class Namespace
254
+ attr_accessor :params
255
+ def initialize(hash)
256
+ @params = hash
257
+ end
258
+ def get_binding
259
+ binding
260
+ end
261
+ end
262
+
263
+ # Combines the provided ERB template with optional parameters
264
+ def erb_template(filename, params = {})
265
+ ERB.new(file(filename), nil, '-').result(Namespace.new(params).get_binding)
266
+ end
267
+
268
+ def warn_deprecated(old, new)
269
+ $stderr.puts "Warning: '#{old}' has been deprecated. Please update your template to use '#{new}' instead."
270
+ end
@@ -0,0 +1,50 @@
1
+ # Copyright 2013-2014 Bazaarvoice, Inc.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ # This list of prices was sourced from the on-demand prices current as of 12/3/2012.
16
+ # We expect the actual price we pay per instance to be roughly 1/10 the prices below.
17
+ SPOT_PRICES_BY_INSTANCE_TYPE = {
18
+ "m1.small" => 0.065,
19
+ "m1.medium" => 0.130,
20
+ "m1.large" => 0.260,
21
+ "m1.xlarge" => 0.520,
22
+ "m3.xlarge" => 0.580,
23
+ "m3.2xlarge" => 1.160,
24
+ "t1.micro" => 0.020,
25
+ "m2.xlarge" => 0.450,
26
+ "m2.2xlarge" => 0.900,
27
+ "m2.4xlarge" => 1.800,
28
+ "c1.medium" => 0.165,
29
+ "c1.xlarge" => 0.660,
30
+ "cc1.4xlarge" => 1.300,
31
+ "cc2.8xlarge" => 2.400,
32
+ "cg1.4xlarge" => 2.100,
33
+ "hi1.4xlarge" => 3.100,
34
+ "hs1.8xlarge" => 4.600,
35
+ "cr1.8xlarge" => 4.000,
36
+ }
37
+
38
+ def spot_price(spot_price_string, instance_type)
39
+ case spot_price_string
40
+ when 'false', '' then aws_no_value()
41
+ when 'true' then spot_price_for_instance_type(instance_type)
42
+ else spot_price_string
43
+ end
44
+ end
45
+
46
+ def spot_price_for_instance_type(instance_type)
47
+ # Add 10% to ensure that we have a small buffer against current spot prices increasing
48
+ # to the on-demand prices, which theoretically could happen often.
49
+ SPOT_PRICES_BY_INSTANCE_TYPE[instance_type] * 1.10
50
+ end
@@ -0,0 +1,123 @@
1
+ # Copyright 2013-2014 Bazaarvoice, Inc.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ require 'detabulator'
16
+
17
+ class Table
18
+ def self.load(filename)
19
+ self.new File.read filename
20
+ end
21
+
22
+ def initialize(table_as_text)
23
+ raw_header, *raw_data = Detabulator.new.detabulate table_as_text
24
+ @header = raw_header.map(&:to_sym)
25
+ @records = raw_data.map { |row| Hash[@header.zip(row)] }
26
+ end
27
+
28
+ # Selects all rows in the table which match the name/value pairs of the predicate object and returns
29
+ # the single distinct value from those rows for the specified key.
30
+ def get(key, predicate)
31
+ distinct_values(filter(predicate), key, false)
32
+ end
33
+
34
+ # Select the headers as list. Argument(s) will be excluded from output.
35
+ def get_header(*exclude)
36
+ @header.reject{ |key| key if exclude.include?(key) }
37
+ end
38
+
39
+ # Selects all rows in the table which match the name/value pairs of the predicate object and returns
40
+ # all distinct values from those rows for the specified key.
41
+ def get_list(key, predicate)
42
+ distinct_values(filter(predicate), key, true)
43
+ end
44
+
45
+ # Selects all rows in the table which match the name/value pairs of the predicate object and returns a
46
+ # hash of hashes, where the key for the top-level hash is the key paramter and the second-level hash keys are
47
+ # those in the keys paramter. This is useful when you want multiple column values for a given row.
48
+ def get_multihash(key, predicate, *keys)
49
+ build_nested_hash(filter(predicate), key, keys)
50
+ end
51
+
52
+ # Selects all rows in the table which match the name/value pairs of the predicate object and returns a
53
+ # set of nested maps, where the key for the map at level n is the key at index n in the specified keys,
54
+ # except for the last key in the specified keys which is used to determine the value of the leaf-level map.
55
+ # In the simple case where keys is a list of 2 elements, this returns a map from key[0] to key[1].
56
+ def get_map(predicate, *keys)
57
+ build_nested_map(filter(predicate), keys, false)
58
+ end
59
+
60
+ # Selects all rows in the table which match the name/value pairs of the predicate object and returns a
61
+ # set of nested maps, where the key for the map at level n is the key at index n in the specified keys,
62
+ # except for the last key in the specified keys which is used to determine the list of values in the
63
+ # leaf-level map. In the simple case where keys is a list of 2 elements, this returns a map from key[0]
64
+ # to a list of values for key[1].
65
+ def get_multimap(predicate, *keys)
66
+ build_nested_map(filter(predicate), keys, true)
67
+ end
68
+
69
+ private
70
+
71
+ # Return the subset of records that match the predicate for all keys in the predicate.
72
+ # The predicate is expected to be a map of key/value or key/[value,...] pairs.
73
+ def filter(predicate)
74
+ def matches(predicate_value, record_value)
75
+ if predicate_value.is_a?(Array); predicate_value.include?(record_value)
76
+ else; predicate_value == record_value
77
+ end
78
+ end
79
+
80
+ @records.select { |record| predicate.all? { |key, value| matches(value, record[key]) } }
81
+ end
82
+
83
+ def build_nested_hash(records, key, keys)
84
+ hash = {}
85
+ records.each do |record|
86
+ hash[record[key]] = {}
87
+ keys.each do |hash_key|
88
+ hash[record[key]][hash_key] = record[hash_key]
89
+ end
90
+ end
91
+ return hash
92
+ end
93
+
94
+ def build_nested_map(records, path, multi)
95
+ key, *rest = path
96
+ if rest.empty?
97
+ # Build the leaf level of the data structure
98
+ distinct_values(records, key, multi)
99
+ else
100
+ # Return a hash keyed by the distinct values of the first key and values are the result of a
101
+ # recursive invocation of arrange() with the rest of the keys
102
+ result = {}
103
+ records.group_by do |record|
104
+ record[key]
105
+ end.map do |value, group|
106
+ result[value] = build_nested_map(group, rest, multi)
107
+ end
108
+ result
109
+ end
110
+ end
111
+
112
+ def distinct_values(records, key, multi)
113
+ values = records.map { |record| record[key] }.uniq
114
+ if multi
115
+ # In a multimap the leaf level is a list of string values
116
+ values
117
+ else
118
+ # In a non-multimap the leaf level is a single string value
119
+ raise "Multiple distinct values for the same key '#{key}': #{records.inspect}" if values.length > 1
120
+ values[0]
121
+ end
122
+ end
123
+ end
@@ -0,0 +1,21 @@
1
+ # Copyright 2013-2014 Bazaarvoice, Inc.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ module Cfn
16
+ module Ruby
17
+ module Dsl
18
+ VERSION = "1.2.4"
19
+ end
20
+ end
21
+ end