cardtapp-cloudformation-ruby-dsl 0.0.1.pre.pre1

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,380 @@
1
+ require 'json'
2
+
3
+ ############################ Utility functions
4
+
5
+ # Formats a template as JSON
6
+ def generate_template(template)
7
+ generate_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(options = {}, &block)
48
+ TemplateDSL.new(options, &block)
49
+ end
50
+
51
+ def default_region
52
+ ENV['EC2_REGION'] || ENV['AWS_DEFAULT_REGION'] || 'us-east-1'
53
+ end
54
+
55
+ class Parameter < String
56
+ attr_accessor :default, :use_previous_value
57
+
58
+ def initialize string
59
+ super string.to_s
60
+ end
61
+ end
62
+
63
+ # Core interpreter for the DSL
64
+ class TemplateDSL < JsonObjectDSL
65
+ attr_reader :parameters,
66
+ :parameter_cli,
67
+ :aws_region,
68
+ :nopretty,
69
+ :stack_name,
70
+ :aws_profile,
71
+ :s3_bucket
72
+
73
+ def initialize(options)
74
+ @parameters = options.fetch(:parameters, {})
75
+ @interactive = options.fetch(:interactive, false)
76
+ @stack_name = options[:stack_name]
77
+ @aws_region = options.fetch(:region, default_region)
78
+ @aws_profile = options[:profile]
79
+ @nopretty = options.fetch(:nopretty, false)
80
+ @s3_bucket = options.fetch(:s3_bucket, nil)
81
+ super()
82
+ end
83
+
84
+ def exec!()
85
+ cfn(self)
86
+ end
87
+
88
+ def parameter(name, options)
89
+ default(:Parameters, {})[name] = options
90
+
91
+ if @interactive
92
+ @parameters[name] ||= Parameter.new(_get_parameter_from_cli(name, options))
93
+ else
94
+ @parameters[name] ||= Parameter.new('')
95
+ end
96
+
97
+ # set various param options
98
+ @parameters[name].default = options[:Default]
99
+ @parameters[name].use_previous_value = options[:UsePreviousValue]
100
+ end
101
+
102
+ # Find parameters where the specified attribute is true then remove the attribute from the cfn template.
103
+ def excise_parameter_attributes!(attributes)
104
+ marked_parameters = {}
105
+ @dict.fetch(:Parameters, {}).each do |param, options|
106
+ attributes.each do |attribute|
107
+ marked_parameters[attribute] ||= []
108
+ if options.delete(attribute.to_sym) or options.delete(attribute.to_s)
109
+ marked_parameters[attribute] << param
110
+ end
111
+ end
112
+ end
113
+ marked_parameters
114
+ end
115
+
116
+ def mapping(name, options)
117
+ # if options is a string and a valid file then the script will process the external file.
118
+ default(:Mappings, {})[name] = \
119
+ if options.is_a?(Hash); options
120
+ elsif options.is_a?(String); load_from_file(options)['Mappings'][name]
121
+ else; raise("Options for mapping #{name} is neither a string or a hash. Error!")
122
+ end
123
+ end
124
+
125
+ def load_from_file(filename)
126
+ file = File.open(filename)
127
+
128
+ begin
129
+ # Figure out what the file extension is and process accordingly.
130
+ contents = case File.extname(filename)
131
+ when ".rb"; eval(file.read, nil, filename)
132
+ when ".json"; JSON.load(file)
133
+ when ".yaml"; YAML::load(file)
134
+ else; raise("Do not recognize extension of #{filename}.")
135
+ end
136
+ ensure
137
+ file.close
138
+ end
139
+ contents
140
+ end
141
+
142
+ # Find tags where the specified attribute is true then remove this attribute.
143
+ def get_tag_attribute(tags, attribute)
144
+ marked_tags = []
145
+ tags.each do |tag, options|
146
+ if options.delete(attribute.to_sym) or options.delete(attribute.to_s)
147
+ marked_tags << tag
148
+ end
149
+ end
150
+ marked_tags
151
+ end
152
+
153
+ def excise_tags!
154
+ tags = @dict.fetch(:Tags, {})
155
+ @dict.delete(:Tags)
156
+ tags
157
+ end
158
+
159
+ def tag(tag, *args)
160
+ if (tag.is_a?(String) || tag.is_a?(Symbol)) && !args.empty?
161
+ default(:Tags, {})[tag.to_s] = args[0]
162
+ # For backward-compatibility, transform `tag_name=>value` format to `tag_name, :Value=>value, :Immutable=>true`
163
+ # Tags declared this way remain immutable and won't be updated.
164
+ elsif tag.is_a?(Hash) && tag.size == 1 && args.empty?
165
+ $stderr.puts "WARNING: #{tag} tag declaration format is deprecated and will be removed in a future version. Please use resource-like style instead."
166
+ tag.each do |name, value|
167
+ default(:Tags, {})[name.to_s] = {:Value => value, :Immutable => true}
168
+ end
169
+ else
170
+ $stderr.puts "Error: #{tag} tag validation error. Please verify tag's declaration format."
171
+ exit(false)
172
+ end
173
+ end
174
+
175
+ def metadata(name, options) default(:Metadata, {})[name] = options end
176
+
177
+ def condition(name, options) default(:Conditions, {})[name] = options end
178
+
179
+ def resource(name, options) default(:Resources, {})[name] = options end
180
+
181
+ def output(name, options) default(:Outputs, {})[name] = options end
182
+
183
+ def find_in_map(map, key, name)
184
+ # Eagerly evaluate mappings when all keys are known at template expansion time
185
+ if map.is_a?(String) && key.is_a?(String) && name.is_a?(String)
186
+ # We don't know whether the map was built with string keys or symbol keys. Try both.
187
+ def get(map, key) map[key] || map.fetch(key.to_sym) end
188
+ get(get(@dict.fetch(:Mappings).fetch(map), key), name)
189
+ else
190
+ { :'Fn::FindInMap' => [ map, key, name ] }
191
+ end
192
+ end
193
+
194
+ private
195
+ def _get_parameter_from_cli(name, options)
196
+ # basic request
197
+ param_request = "Parameter '#{name}' (#{options[:Type]})"
198
+
199
+ # add description to request
200
+ if options.has_key?(:Description)
201
+ param_request += "\nDescription: #{options[:Description]}"
202
+ end
203
+
204
+ # add validation to the request
205
+
206
+ # allowed pattern
207
+ if options.has_key?(:AllowedPattern)
208
+ param_request += "\nAllowed Pattern: /#{options[:AllowedPattern]}/"
209
+ end
210
+
211
+ # allowed values
212
+ if options.has_key?(:AllowedValues)
213
+ param_request += "\nAllowed Values: #{options[:AllowedValues].join(', ')}"
214
+ end
215
+
216
+ # min/max length
217
+ if options.has_key?(:MinLength) or options.has_key?(:MaxLength)
218
+ min_length = "-infinity"
219
+ max_length = "+infinity"
220
+ if options.has_key?(:MinLength)
221
+ min_length = options[:MinLength]
222
+ end
223
+ if options.has_key?(:MaxLength)
224
+ max_length = options[:MaxLength]
225
+ end
226
+ param_request += "\nValid Length: #{min_length} < string < #{max_length}"
227
+ end
228
+
229
+ # min/max value
230
+ if options.has_key?(:MinValue) or options.has_key?(:MaxValue)
231
+ min_value = "-infinity"
232
+ max_value = "+infinity"
233
+ if options.has_key?(:MinValue)
234
+ min_value = options[:MinValue]
235
+ end
236
+ if options.has_key?(:MaxValue)
237
+ max_value = options[:MaxValue]
238
+ end
239
+ param_request += "\nValid Number: #{min_value} < number < #{max_value}"
240
+ end
241
+
242
+ # add default to request
243
+ if options.has_key?(:Default) and !options[:Default].nil?
244
+ param_request += "\nLeave value empty for default: #{options[:Default]}"
245
+ end
246
+
247
+ param_request += "\nValue: "
248
+
249
+ # request the param
250
+ $stdout.puts "===================="
251
+ $stdout.print param_request
252
+ input = $stdin.gets.chomp
253
+
254
+ if input.nil? or input.empty?
255
+ options[:Default]
256
+ else
257
+ input
258
+ end
259
+ end
260
+ end
261
+
262
+ def base64(value) { :'Fn::Base64' => value } end
263
+
264
+ def find_in_map(map, key, name) { :'Fn::FindInMap' => [ map, key, name ] } end
265
+
266
+ def get_att(resource, attribute) { :'Fn::GetAtt' => [ resource, attribute ] } end
267
+
268
+ def get_azs(region = '') { :'Fn::GetAZs' => region } end
269
+
270
+ def import_value(value) { :'Fn::ImportValue' => value } end
271
+
272
+ # There are two valid forms of Fn::Sub, with a map and without.
273
+ def sub(sub_string, var_map = {})
274
+ if var_map.empty?
275
+ return { :'Fn::Sub' => sub_string }
276
+ else
277
+ return { :'Fn::Sub' => [sub_string, var_map] }
278
+ end
279
+ end
280
+
281
+ def join(delim, *list)
282
+ case list.length
283
+ when 0 then ''
284
+ else join_list(delim,list)
285
+ end
286
+ end
287
+
288
+ # Variant of join that matches the native CFN syntax.
289
+ def join_list(delim, list) { :'Fn::Join' => [ delim, list ] } end
290
+
291
+ def equal(one, two) { :'Fn::Equals' => [one, two] } end
292
+
293
+ def fn_not(condition) { :'Fn::Not' => [condition] } end
294
+
295
+ def fn_or(*condition_list)
296
+ case condition_list.length
297
+ when 0..1 then raise "fn_or needs at least 2 items."
298
+ when 2..10 then { :'Fn::Or' => condition_list }
299
+ else raise "fn_or needs a list of 2-10 items that evaluate to true/false."
300
+ end
301
+ end
302
+
303
+ def fn_and(*condition_list)
304
+ case condition_list.length
305
+ when 0..1 then raise "fn_and needs at least 2 items."
306
+ when 2..10 then { :'Fn::And' => condition_list }
307
+ else raise "fn_and needs a list of 2-10 items that evaluate to true/false."
308
+ end
309
+ end
310
+
311
+ def fn_if(cond, if_true, if_false) { :'Fn::If' => [cond, if_true, if_false] } end
312
+
313
+ def not_equal(one, two) fn_not(equal(one,two)) end
314
+
315
+ def select(index, list) { :'Fn::Select' => [ index, list ] } end
316
+
317
+ def split(delimiter, source_str) { :'Fn::Split' => [delimiter, source_str] } end
318
+
319
+ def ref(name) { :Ref => name } end
320
+
321
+ def aws_account_id() ref("AWS::AccountId") end
322
+
323
+ def aws_notification_arns() ref("AWS::NotificationARNs") end
324
+
325
+ def aws_no_value() ref("AWS::NoValue") end
326
+
327
+ def aws_stack_id() ref("AWS::StackId") end
328
+
329
+ def aws_stack_name() ref("AWS::StackName") end
330
+
331
+ # deprecated, for backward compatibility
332
+ def no_value()
333
+ warn_deprecated('no_value()', 'aws_no_value()')
334
+ aws_no_value()
335
+ end
336
+
337
+ # Read the specified file and return its value as a string literal
338
+ def file(filename) File.read(File.absolute_path(filename, File.dirname($PROGRAM_NAME))) end
339
+
340
+ # Interpolates a string like "NAME={{ref('Service')}}" and returns a CloudFormation "Fn::Join"
341
+ # operation to collect the results. Anything between {{ and }} is interpreted as a Ruby expression
342
+ # and eval'd. This is especially useful with Ruby "here" documents.
343
+ # Local variables may also be exposed to the string via the `locals` hash.
344
+ def interpolate(string, locals={})
345
+ list = []
346
+ while string.length > 0
347
+ head, match, string = string.partition(/\{\{.*?\}\}/)
348
+ list << head if head.length > 0
349
+ list << eval(match[2..-3], nil, 'interpolated string') if match.length > 0
350
+ end
351
+
352
+ # Split out strings in an array by newline, for visibility
353
+ list = list.flat_map {|value| value.is_a?(String) ? value.lines.to_a : value }
354
+ join('', *list)
355
+ end
356
+
357
+ def join_interpolate(delim, string)
358
+ $stderr.puts "join_interpolate(delim,string) has been deprecated; use interpolate(string) instead"
359
+ interpolate(string)
360
+ end
361
+
362
+ # This class is used by erb templates so they can access the parameters passed
363
+ class Namespace
364
+ attr_accessor :params
365
+ def initialize(hash)
366
+ @params = hash
367
+ end
368
+ def get_binding
369
+ binding
370
+ end
371
+ end
372
+
373
+ # Combines the provided ERB template with optional parameters
374
+ def erb_template(filename, params = {})
375
+ ERB.new(file(filename), nil, '-').result(Namespace.new(params).get_binding)
376
+ end
377
+
378
+ def warn_deprecated(old, new)
379
+ $stderr.puts "Warning: '#{old}' has been deprecated. Please update your template to use '#{new}' instead."
380
+ 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 = "0.0.1-pre1"
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,2 @@
1
+ require "cloudformation-ruby-dsl/version"
2
+