convection 0.0.1 → 0.2.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +1 -0
- data/.rubocop.yml +26 -8
- data/.rubocop_todo.yml +77 -0
- data/.ruby-version +1 -0
- data/.travis.yml +10 -0
- data/Gemfile +9 -0
- data/README.md +27 -2
- data/Rakefile +11 -1
- data/bin/convection +49 -0
- data/convection.gemspec +5 -7
- data/example/.ruby-version +1 -0
- data/example/Cloudfile +13 -0
- data/example/deprecated/elb.rb +27 -0
- data/example/deprecated/iam_access_key.rb +18 -0
- data/example/deprecated/iam_group.rb +31 -0
- data/example/{iam_role.rb → deprecated/iam_role.rb} +21 -32
- data/example/deprecated/iam_user.rb +31 -0
- data/example/deprecated/rds.rb +70 -0
- data/example/{s3.rb → deprecated/s3.rb} +0 -0
- data/example/deprecated/sqs.rb +32 -0
- data/example/deprecated/vpc.rb +85 -0
- data/example/foobar.rb +22 -0
- data/example/output/vpc.json +335 -0
- data/example/security-groups.rb +40 -0
- data/example/trust_cloudtrail.rb +24 -0
- data/example/vpc.rb +63 -81
- data/ext/resource_generator.sh +21 -0
- data/lib/convection.rb +5 -4
- data/lib/convection/control/cloud.rb +59 -0
- data/lib/convection/control/stack.rb +261 -60
- data/lib/convection/dsl/helpers.rb +63 -5
- data/lib/convection/model/attributes.rb +60 -0
- data/lib/convection/model/cloudfile.rb +58 -0
- data/lib/convection/model/diff.rb +39 -0
- data/lib/convection/model/event.rb +62 -0
- data/lib/convection/model/exceptions.rb +18 -0
- data/lib/convection/model/mixin/cidr_block.rb +4 -4
- data/lib/convection/model/mixin/colorize.rb +20 -0
- data/lib/convection/model/mixin/conditional.rb +1 -3
- data/lib/convection/model/mixin/policy.rb +89 -0
- data/lib/convection/model/mixin/protocol.rb +29 -0
- data/lib/convection/model/mixin/taggable.rb +2 -2
- data/lib/convection/model/template.rb +248 -21
- data/lib/convection/model/template/condition.rb +56 -0
- data/lib/convection/model/template/mapping.rb +4 -3
- data/lib/convection/model/template/output.rb +9 -7
- data/lib/convection/model/template/parameter.rb +19 -4
- data/lib/convection/model/template/resource.rb +317 -23
- data/lib/convection/model/template/resource/aws_auto_scaling_auto_scaling_group.rb +39 -0
- data/lib/convection/model/template/resource/aws_auto_scaling_launch_configuration.rb +30 -0
- data/lib/convection/model/template/resource/aws_auto_scaling_scaling_policy.rb +20 -0
- data/lib/convection/model/template/resource/aws_cloud_watch_alarm.rb +31 -0
- data/lib/convection/model/template/resource/aws_ec2_instance.rb +10 -46
- data/lib/convection/model/template/resource/aws_ec2_internet_gateway.rb +3 -14
- data/lib/convection/model/template/resource/aws_ec2_network_acl.rb +45 -0
- data/lib/convection/model/template/resource/aws_ec2_network_acl_entry.rb +27 -0
- data/lib/convection/model/template/resource/aws_ec2_route.rb +7 -40
- data/lib/convection/model/template/resource/aws_ec2_route_table.rb +2 -17
- data/lib/convection/model/template/resource/aws_ec2_security_group.rb +24 -30
- data/lib/convection/model/template/resource/aws_ec2_security_group_ingres.rb +25 -0
- data/lib/convection/model/template/resource/aws_ec2_subnet.rb +21 -28
- data/lib/convection/model/template/resource/aws_ec2_subnet_network_acl_association.rb +18 -0
- data/lib/convection/model/template/resource/aws_ec2_subnet_route_table_association.rb +3 -24
- data/lib/convection/model/template/resource/aws_ec2_vpc.rb +20 -22
- data/lib/convection/model/template/resource/aws_ec2_vpc_gateway_attachment.rb +4 -28
- data/lib/convection/model/template/resource/aws_elasticache_cluster.rb +24 -0
- data/lib/convection/model/template/resource/aws_elasticache_parameter_group.rb +19 -0
- data/lib/convection/model/template/resource/aws_elasticache_security_group.rb +17 -0
- data/lib/convection/model/template/resource/aws_elasticache_security_group_ingress.rb +19 -0
- data/lib/convection/model/template/resource/aws_elb.rb +39 -0
- data/lib/convection/model/template/resource/aws_iam_access_key.rb +19 -0
- data/lib/convection/model/template/resource/aws_iam_group.rb +18 -0
- data/lib/convection/model/template/resource/aws_iam_instance_profile.rb +21 -0
- data/lib/convection/model/template/resource/aws_iam_policy.rb +28 -24
- data/lib/convection/model/template/resource/aws_iam_role.rb +88 -19
- data/lib/convection/model/template/resource/aws_iam_user.rb +53 -0
- data/lib/convection/model/template/resource/aws_logs_loggroup.rb +33 -0
- data/lib/convection/model/template/resource/aws_rds_db_instance.rb +59 -0
- data/lib/convection/model/template/resource/aws_rds_db_parameter_group.rb +27 -0
- data/lib/convection/model/template/resource/aws_rds_db_security_group.rb +40 -0
- data/lib/convection/model/template/resource/aws_rds_db_subnet_group.rb +26 -0
- data/lib/convection/model/template/resource/aws_route53_health_check.rb +17 -0
- data/lib/convection/model/template/resource/aws_route53_recordset.rb +30 -0
- data/lib/convection/model/template/resource/aws_s3_bucket.rb +8 -44
- data/lib/convection/model/template/resource/aws_s3_bucket_policy.rb +14 -19
- data/lib/convection/model/template/resource/aws_sns_topic.rb +19 -0
- data/lib/convection/model/template/resource/aws_sqs_queue.rb +31 -0
- data/lib/convection/model/template/resource/aws_sqs_queue_policy.rb +18 -0
- data/test/convection/model/test_conditions.rb +121 -0
- data/test/convection/model/test_elasticache.rb +97 -0
- data/test/convection/model/test_loggroups.rb +25 -0
- data/test/convection/model/test_rds.rb +76 -0
- data/test/convection/model/test_template.rb +64 -0
- data/test/convection/model/test_validation.rb +216 -0
- data/test/test_helper.rb +17 -0
- metadata +131 -50
@@ -0,0 +1,29 @@
|
|
1
|
+
require 'netaddr'
|
2
|
+
|
3
|
+
module Convection
|
4
|
+
module Model
|
5
|
+
module Mixin
|
6
|
+
##
|
7
|
+
# Map IP protocol names to numbers
|
8
|
+
##
|
9
|
+
module Protocol
|
10
|
+
class << self
|
11
|
+
def lookup(value)
|
12
|
+
case value
|
13
|
+
when :any then -1
|
14
|
+
when :icmp then 1
|
15
|
+
when :tcp then 6
|
16
|
+
when :udp then 17
|
17
|
+
else value
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def protocol_property(name = :protocol, property_name = 'IpProtocol')
|
23
|
+
property(name, property_name,
|
24
|
+
:transform => Mixin::Protocol.method(:lookup))
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -1,5 +1,7 @@
|
|
1
1
|
require_relative '../dsl/helpers'
|
2
2
|
require_relative '../dsl/intrinsic_functions'
|
3
|
+
require_relative './diff'
|
4
|
+
require_relative './exceptions.rb'
|
3
5
|
require 'json'
|
4
6
|
|
5
7
|
module Convection
|
@@ -8,6 +10,44 @@ module Convection
|
|
8
10
|
# Template DSL
|
9
11
|
##
|
10
12
|
module Template
|
13
|
+
##
|
14
|
+
# Container for DSL interfaces
|
15
|
+
##
|
16
|
+
module Resource
|
17
|
+
class << self
|
18
|
+
## Wrap private define_method
|
19
|
+
def attach_resource(name, klass)
|
20
|
+
define_method(name) do |rname, &block|
|
21
|
+
resource = klass.new(rname, self)
|
22
|
+
resource.instance_exec(&block) if block
|
23
|
+
|
24
|
+
resources[rname] = resource
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
include DSL::Helpers
|
31
|
+
include DSL::Template::Resource
|
32
|
+
|
33
|
+
CF_MAX_BYTESIZE = 51_200
|
34
|
+
CF_MAX_DESCRIPTION_BYTESIZE = 1_024
|
35
|
+
CF_MAX_MAPPING_ATTRIBUTE_NAME = 255
|
36
|
+
CF_MAX_MAPPING_ATTRIBUTES = 30
|
37
|
+
CF_MAX_MAPPING_NAME = 25
|
38
|
+
CF_MAX_MAPPINGS = 100
|
39
|
+
CF_MAX_OUTPUT_NAME_CHARACTERS = 255
|
40
|
+
CF_MAX_OUTPUTS = 60
|
41
|
+
CF_MAX_PARAMETER_NAME_CHARACTERS = 255
|
42
|
+
CF_MAX_PARAMETERS = 60
|
43
|
+
CF_MAX_PARAMETER_VALUE_BYTESIZE = 4_086
|
44
|
+
CF_MAX_RESOURCE_NAME = 255
|
45
|
+
CF_MAX_RESOURCES = 200
|
46
|
+
|
47
|
+
attribute :name
|
48
|
+
attribute :version
|
49
|
+
attribute :description
|
50
|
+
|
11
51
|
def parameter(name, &block)
|
12
52
|
pa = Model::Template::Parameter.new(name, self)
|
13
53
|
|
@@ -22,12 +62,12 @@ module Convection
|
|
22
62
|
mappings[name] = m
|
23
63
|
end
|
24
64
|
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
65
|
+
def condition(name, &block)
|
66
|
+
c = Model::Template::Condition.new(name, self)
|
67
|
+
|
68
|
+
c.instance_exec(&block) if block
|
69
|
+
conditions[name] = c
|
70
|
+
end
|
31
71
|
|
32
72
|
def resource(name, &block)
|
33
73
|
r = Model::Template::Resource.new(name, self)
|
@@ -50,42 +90,106 @@ module Convection
|
|
50
90
|
# Mapable hash
|
51
91
|
##
|
52
92
|
class Collection < Hash
|
53
|
-
def map(&block)
|
93
|
+
def map(no_nil = false, &block)
|
54
94
|
result = {}
|
55
95
|
|
56
96
|
each do |key, value|
|
57
|
-
|
97
|
+
res = block.call(value)
|
98
|
+
|
99
|
+
next if no_nil && res.nil?
|
100
|
+
next if no_nil && res.is_a?(Array) && res.empty?
|
101
|
+
next if no_nil && res.is_a?(Hash) && res.empty?
|
102
|
+
|
103
|
+
result[key] = res
|
58
104
|
end
|
59
105
|
|
60
106
|
result
|
61
107
|
end
|
62
108
|
end
|
63
109
|
|
110
|
+
##
|
111
|
+
# HACK: Add generic diff(other) and properties to Hash and Array
|
112
|
+
##
|
113
|
+
class ::Array
|
114
|
+
## Recursivly flatten an array into 1st order key/value pairs
|
115
|
+
def properties(memo = {}, path = '')
|
116
|
+
each_with_index do |elm, i|
|
117
|
+
if elm.is_a?(Hash) || elm.is_a?(Array)
|
118
|
+
elm.properties(memo, "#{path}.#{i}")
|
119
|
+
else
|
120
|
+
memo["#{path}.#{i}"] = elm
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
memo
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
##
|
129
|
+
# HACK: Add generic diff(other) and properties to Hash and Array
|
130
|
+
##
|
131
|
+
class ::Hash
|
132
|
+
## Use flattened properties to calculate a diff
|
133
|
+
def diff(other = {})
|
134
|
+
our_properties = properties
|
135
|
+
their_properties = other.properties
|
136
|
+
|
137
|
+
(our_properties.keys + their_properties.keys).uniq.each_with_object({}) do |key, memo|
|
138
|
+
next if (our_properties[key] == their_properties[key] rescue false)
|
139
|
+
|
140
|
+
## HACK: String/Number/Symbol comparison
|
141
|
+
if our_properties[key].is_a?(Numeric) ||
|
142
|
+
their_properties[key].is_a?(Numeric) ||
|
143
|
+
our_properties[key].is_a?(Symbol) ||
|
144
|
+
their_properties[key].is_a?(Symbol)
|
145
|
+
next if our_properties[key].to_s == their_properties[key].to_s
|
146
|
+
end
|
147
|
+
|
148
|
+
memo[key] = [our_properties[key], their_properties[key]]
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
## Recursivly flatten a hash into 1st order key/value pairs
|
153
|
+
def properties(memo = {}, path = '')
|
154
|
+
keys.each do |key|
|
155
|
+
if self[key].is_a?(Hash) || self[key].is_a?(Array)
|
156
|
+
self[key].properties(memo, "#{path}.#{key}")
|
157
|
+
else
|
158
|
+
memo["#{path}.#{key}"] = self[key]
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
memo
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
64
166
|
##
|
65
167
|
# Template container class
|
66
168
|
##
|
67
169
|
class Template
|
68
|
-
extend DSL::Helpers
|
69
|
-
|
70
170
|
include DSL::IntrinsicFunctions
|
71
171
|
include DSL::Template
|
72
172
|
|
73
173
|
DEFAULT_VERSION = '2010-09-09'
|
74
174
|
|
75
|
-
attribute :version
|
76
|
-
attribute :description
|
77
|
-
attribute :region
|
78
|
-
|
79
175
|
attr_reader :stack
|
176
|
+
attr_reader :attribute_mappings
|
177
|
+
|
80
178
|
attr_reader :parameters
|
81
179
|
attr_reader :mappings
|
82
180
|
attr_reader :conditions
|
83
181
|
attr_reader :resources
|
84
182
|
attr_reader :outputs
|
85
183
|
|
86
|
-
def
|
184
|
+
def template
|
185
|
+
self
|
186
|
+
end
|
187
|
+
|
188
|
+
def initialize(stack = nil, &block)
|
87
189
|
@definition = block
|
190
|
+
|
88
191
|
@stack = stack
|
192
|
+
@attribute_mappings = {}
|
89
193
|
|
90
194
|
@version = DEFAULT_VERSION
|
91
195
|
@description = ''
|
@@ -97,11 +201,20 @@ module Convection
|
|
97
201
|
@outputs = Collection.new
|
98
202
|
end
|
99
203
|
|
100
|
-
def
|
101
|
-
|
102
|
-
|
204
|
+
def clone(stack_)
|
205
|
+
Template.new(stack_, &@definition)
|
206
|
+
end
|
103
207
|
|
208
|
+
def execute
|
104
209
|
instance_exec(&@definition)
|
210
|
+
end
|
211
|
+
|
212
|
+
def render(stack_ = nil)
|
213
|
+
## Instantiate a new template with the definition block and an other stack
|
214
|
+
return clone(stack_).render unless stack_.nil?
|
215
|
+
|
216
|
+
execute ## Process the template document
|
217
|
+
|
105
218
|
{
|
106
219
|
'AWSTemplateFormatVersion' => version,
|
107
220
|
'Description' => description,
|
@@ -113,8 +226,122 @@ module Convection
|
|
113
226
|
}
|
114
227
|
end
|
115
228
|
|
116
|
-
def
|
117
|
-
|
229
|
+
def diff(other, stack_ = nil)
|
230
|
+
render(stack_).diff(other).map { |diff| Diff.new(diff[0], *diff[1]) }
|
231
|
+
end
|
232
|
+
|
233
|
+
def to_json(stack_ = nil, pretty = false)
|
234
|
+
rendered_stack = render(stack_)
|
235
|
+
validate(rendered_stack)
|
236
|
+
return JSON.generate(rendered_stack) unless pretty
|
237
|
+
JSON.pretty_generate(rendered_stack)
|
238
|
+
end
|
239
|
+
|
240
|
+
def validate(rendered_stack = nil)
|
241
|
+
%w(resources mappings parameters outputs description bytesize).map do |method|
|
242
|
+
send("validate_#{method}", rendered_stack)
|
243
|
+
end
|
244
|
+
end
|
245
|
+
|
246
|
+
def validate_compare(value, cf_max, error)
|
247
|
+
limit_exceeded_error(value, cf_max, error) if value > cf_max
|
248
|
+
end
|
249
|
+
|
250
|
+
def validate_resources(rendered_stack)
|
251
|
+
validate_compare(
|
252
|
+
rendered_stack['Resources'].count,
|
253
|
+
CF_MAX_RESOURCES,
|
254
|
+
ExcessiveResourcesError)
|
255
|
+
|
256
|
+
largest_resource_name = resources.keys.max || ''
|
257
|
+
validate_compare(
|
258
|
+
largest_resource_name.length,
|
259
|
+
CF_MAX_RESOURCE_NAME,
|
260
|
+
ExcessiveResourceNameError)
|
261
|
+
end
|
262
|
+
|
263
|
+
def validate_mappings(rendered_stack)
|
264
|
+
mappings = rendered_stack ['Mappings']
|
265
|
+
validate_compare(
|
266
|
+
mappings.count,
|
267
|
+
CF_MAX_MAPPINGS,
|
268
|
+
ExcessiveMappingsError)
|
269
|
+
mappings.each do |_, value|
|
270
|
+
validate_compare(
|
271
|
+
value.count,
|
272
|
+
CF_MAX_MAPPING_ATTRIBUTES,
|
273
|
+
ExcessiveMappingAttributesError)
|
274
|
+
end
|
275
|
+
|
276
|
+
mappings.keys.each do |key|
|
277
|
+
validate_compare(
|
278
|
+
key.length,
|
279
|
+
CF_MAX_MAPPING_NAME,
|
280
|
+
ExcessiveMappingNameError)
|
281
|
+
end
|
282
|
+
|
283
|
+
## XXX What are we trying to do here @aburke
|
284
|
+
mapping_attributes = mappings.values.flat_map do |inner_hash|
|
285
|
+
inner_hash.keys.select do |key|
|
286
|
+
value = inner_hash[key]
|
287
|
+
end
|
288
|
+
end
|
289
|
+
|
290
|
+
mapping_attributes.each do |attribute|
|
291
|
+
validate_compare(
|
292
|
+
attribute.length,
|
293
|
+
CF_MAX_MAPPING_ATTRIBUTE_NAME,
|
294
|
+
ExcessiveMappingAttributeNameError)
|
295
|
+
end
|
296
|
+
end
|
297
|
+
|
298
|
+
def validate_parameters(rendered_stack)
|
299
|
+
parameters = rendered_stack['Parameters']
|
300
|
+
validate_compare(
|
301
|
+
parameters.count,
|
302
|
+
CF_MAX_PARAMETERS,
|
303
|
+
ExcessiveParametersError)
|
304
|
+
largest_parameter_name = parameters.keys.max
|
305
|
+
largest_parameter_name ||= ''
|
306
|
+
validate_compare(
|
307
|
+
largest_parameter_name.length,
|
308
|
+
CF_MAX_PARAMETER_NAME_CHARACTERS,
|
309
|
+
ExcessiveParameterNameError)
|
310
|
+
parameters.values.each do |value|
|
311
|
+
validate_compare(
|
312
|
+
JSON.generate(value).bytesize,
|
313
|
+
CF_MAX_PARAMETER_VALUE_BYTESIZE,
|
314
|
+
ExcessiveParameterBytesizeError)
|
315
|
+
end
|
316
|
+
end
|
317
|
+
|
318
|
+
def validate_outputs(rendered_stack)
|
319
|
+
outputs = rendered_stack['Outputs']
|
320
|
+
validate_compare(
|
321
|
+
outputs.count,
|
322
|
+
CF_MAX_OUTPUTS,
|
323
|
+
ExcessiveOutputsError)
|
324
|
+
largest_output_name = outputs.keys.max
|
325
|
+
largest_output_name ||= ''
|
326
|
+
validate_compare(
|
327
|
+
largest_output_name.length,
|
328
|
+
CF_MAX_OUTPUT_NAME_CHARACTERS,
|
329
|
+
ExcessiveOutputNameError)
|
330
|
+
end
|
331
|
+
|
332
|
+
def validate_description(rendered_stack)
|
333
|
+
validate_compare(
|
334
|
+
rendered_stack['Description'].bytesize,
|
335
|
+
CF_MAX_DESCRIPTION_BYTESIZE,
|
336
|
+
ExcessiveDescriptionError)
|
337
|
+
end
|
338
|
+
|
339
|
+
def validate_bytesize(rendered_stack)
|
340
|
+
json = JSON.generate(rendered_stack)
|
341
|
+
validate_compare(
|
342
|
+
json.bytesize,
|
343
|
+
CF_MAX_BYTESIZE,
|
344
|
+
ExcessiveTemplateSizeError)
|
118
345
|
end
|
119
346
|
end
|
120
347
|
end
|
@@ -122,6 +349,6 @@ end
|
|
122
349
|
|
123
350
|
require_relative 'template/parameter'
|
124
351
|
require_relative 'template/mapping'
|
125
|
-
|
352
|
+
require_relative 'template/condition'
|
126
353
|
require_relative 'template/resource'
|
127
354
|
require_relative 'template/output'
|
@@ -0,0 +1,56 @@
|
|
1
|
+
require_relative '../../dsl/intrinsic_functions'
|
2
|
+
|
3
|
+
module Convection
|
4
|
+
module Model
|
5
|
+
class Template
|
6
|
+
class Condition
|
7
|
+
include DSL::Helpers
|
8
|
+
|
9
|
+
CONDITIONAL_FUNCTION_SYNTAX_MAP =
|
10
|
+
{ fn_and: 'Fn::And',
|
11
|
+
fn_equals: 'Fn::Equals',
|
12
|
+
fn_if: 'Fn::If',
|
13
|
+
fn_not: 'Fn::Not',
|
14
|
+
fn_or: 'Fn::Or' }
|
15
|
+
|
16
|
+
attr_reader :condition
|
17
|
+
attr_reader :template
|
18
|
+
|
19
|
+
CONDITIONAL_FUNCTION_SYNTAX_MAP.keys.each do |conditional_function|
|
20
|
+
define_method(conditional_function) do |*args|
|
21
|
+
@condition = ConditionalFunction.new conditional_function, args
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def initialize(name, parent)
|
26
|
+
@name = name
|
27
|
+
@template = parent.template
|
28
|
+
end
|
29
|
+
|
30
|
+
def render
|
31
|
+
condition.render
|
32
|
+
end
|
33
|
+
|
34
|
+
class ConditionalFunction
|
35
|
+
def initialize(function_name, arg_array)
|
36
|
+
@function_name = function_name
|
37
|
+
@function_arguments = arg_array
|
38
|
+
end
|
39
|
+
|
40
|
+
def render
|
41
|
+
rendered_values = []
|
42
|
+
@function_arguments.each do |function_arg|
|
43
|
+
if function_arg.respond_to? :render # the argument is another conditional function
|
44
|
+
rendered_values << function_arg.render
|
45
|
+
else
|
46
|
+
rendered_values << function_arg
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
{ CONDITIONAL_FUNCTION_SYNTAX_MAP[@function_name] => rendered_values }
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -18,13 +18,14 @@ module Convection
|
|
18
18
|
# Mapping
|
19
19
|
##
|
20
20
|
class Mapping
|
21
|
-
include DSL::
|
21
|
+
include DSL::Helpers
|
22
22
|
|
23
23
|
attr_reader :items
|
24
|
+
attr_reader :template
|
24
25
|
|
25
|
-
def initialize(name,
|
26
|
+
def initialize(name, parent)
|
26
27
|
@name = name
|
27
|
-
@template = template
|
28
|
+
@template = parent.template
|
28
29
|
|
29
30
|
@items = Smash.new
|
30
31
|
end
|