regentanz 0.3.3 → 1.0.0
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.
- checksums.yaml +7 -0
- data/README.md +268 -0
- data/bin/regentanz +16 -0
- data/lib/regentanz.rb +10 -11
- data/lib/regentanz/cli/common.rb +35 -0
- data/lib/regentanz/cli/compare.rb +85 -0
- data/lib/regentanz/cli/compile.rb +27 -0
- data/lib/regentanz/template_compiler.rb +263 -0
- data/lib/regentanz/version.rb +1 -2
- data/lib/regentanz/yaml-ext.rb +18 -0
- data/spec/regentanz/resources/test/unloaded.rb +11 -0
- data/spec/regentanz/template_compiler_spec.rb +692 -0
- data/spec/spec_helper.rb +2 -0
- metadata +45 -152
- data/.gitignore +0 -5
- data/.rvmrc +0 -4
- data/CHANGELOG.rdoc +0 -26
- data/Gemfile +0 -4
- data/LICENSE +0 -24
- data/README.rdoc +0 -54
- data/Rakefile +0 -23
- data/lib/regentanz/astronomy.rb +0 -69
- data/lib/regentanz/cache.rb +0 -2
- data/lib/regentanz/cache/base.rb +0 -51
- data/lib/regentanz/cache/file.rb +0 -86
- data/lib/regentanz/callbacks.rb +0 -18
- data/lib/regentanz/conditions.rb +0 -3
- data/lib/regentanz/conditions/base.rb +0 -16
- data/lib/regentanz/conditions/current.rb +0 -14
- data/lib/regentanz/conditions/forecast.rb +0 -14
- data/lib/regentanz/configuration.rb +0 -55
- data/lib/regentanz/configurator.rb +0 -22
- data/lib/regentanz/google_weather.rb +0 -151
- data/lib/regentanz/parser.rb +0 -1
- data/lib/regentanz/parser/google_weather.rb +0 -100
- data/lib/regentanz/test_helper.rb +0 -52
- data/regentanz.gemspec +0 -30
- data/test/factories.rb +0 -6
- data/test/support/tmp/.gitignore +0 -1
- data/test/support/valid_response.xml.erb +0 -26
- data/test/test_helper.rb +0 -14
- data/test/unit/astronomy_test.rb +0 -26
- data/test/unit/cache/base_test.rb +0 -53
- data/test/unit/cache/file_test.rb +0 -141
- data/test/unit/callbacks_test.rb +0 -27
- data/test/unit/configuration_test.rb +0 -57
- data/test/unit/current_condition_test.rb +0 -33
- data/test/unit/forecast_condition_test.rb +0 -35
- data/test/unit/google_weather_test.rb +0 -125
- data/test/unit/parser/google_weather_parser_test.rb +0 -71
@@ -0,0 +1,263 @@
|
|
1
|
+
module Regentanz
|
2
|
+
class TemplateCompiler
|
3
|
+
ParseError = Class.new(Regentanz::Error)
|
4
|
+
ValidationError = Class.new(Regentanz::Error)
|
5
|
+
AmbiguityError = Class.new(Regentanz::Error)
|
6
|
+
CredentialsError = Class.new(Regentanz::Error)
|
7
|
+
TemplateError = Class.new(Regentanz::Error)
|
8
|
+
|
9
|
+
def initialize(config, cloud_formation_client: nil, s3_client: nil)
|
10
|
+
@resource_compilers = {}
|
11
|
+
@region = config['default_region']
|
12
|
+
@template_url = config['template_url']
|
13
|
+
@cf_client = cloud_formation_client || Aws::CloudFormation::Client.new(region: @region)
|
14
|
+
@s3_client = s3_client || Aws::S3::Resource.new(region: @region)
|
15
|
+
rescue Aws::Sigv4::Errors::MissingCredentialsError => e
|
16
|
+
raise CredentialsError, 'Validation requires AWS credentials'
|
17
|
+
end
|
18
|
+
|
19
|
+
def compile_from_path(stack_path)
|
20
|
+
resources = []
|
21
|
+
options = {}
|
22
|
+
Dir.chdir(stack_path) do
|
23
|
+
options[:parameters] = load_top_level_file('parameters')
|
24
|
+
options[:mappings] = load_top_level_file('mappings')
|
25
|
+
options[:conditions] = load_top_level_file('conditions')
|
26
|
+
options[:outputs] = load_top_level_file('outputs')
|
27
|
+
resources = load_resources
|
28
|
+
compile_template(resources, options)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def compile_template(resources, options = {})
|
33
|
+
template = {'AWSTemplateFormatVersion' => '2010-09-09'}
|
34
|
+
compiled = compile_resources(resources)
|
35
|
+
template['Resources'] = compiled.delete(:resources)
|
36
|
+
options = compiled.merge(options) { |_, v1, v2| v1.merge(v2 || {}) }
|
37
|
+
if (parameters = options[:parameters]) && !parameters.empty?
|
38
|
+
parameters, metadata = compile_parameters(parameters)
|
39
|
+
template['Parameters'] = parameters
|
40
|
+
template['Metadata'] = {'AWS::CloudFormation::Interface' => metadata}
|
41
|
+
end
|
42
|
+
template['Mappings'] = expand_refs(options[:mappings]) if options[:mappings]
|
43
|
+
template['Conditions'] = expand_refs(options[:conditions]) if options[:conditions]
|
44
|
+
template['Outputs'] = expand_refs(options[:outputs]) if options[:outputs]
|
45
|
+
validate_parameter_use(template)
|
46
|
+
template
|
47
|
+
end
|
48
|
+
|
49
|
+
def validate_template(stack_path, template)
|
50
|
+
if template.bytesize > 460800
|
51
|
+
raise TemplateError, "Compiled template is too large: #{template.bytesize} bytes > 460800"
|
52
|
+
elsif template.bytesize >= 51200
|
53
|
+
template_url = upload_template(stack_path, template)
|
54
|
+
@cf_client.validate_template(template_url: template_url)
|
55
|
+
else
|
56
|
+
@cf_client.validate_template(template_body: template)
|
57
|
+
end
|
58
|
+
rescue Aws::CloudFormation::Errors::ValidationError => e
|
59
|
+
raise ValidationError, "Invalid template: #{e.message}", e.backtrace
|
60
|
+
end
|
61
|
+
|
62
|
+
private
|
63
|
+
|
64
|
+
PSEUDO_PARAMETERS = [
|
65
|
+
'AWS::AccountId',
|
66
|
+
'AWS::NotificationARNs',
|
67
|
+
'AWS::NoValue',
|
68
|
+
'AWS::Region',
|
69
|
+
'AWS::StackId',
|
70
|
+
'AWS::StackName',
|
71
|
+
].map!(&:freeze).freeze
|
72
|
+
|
73
|
+
def upload_template(stack_path, template)
|
74
|
+
if @template_url
|
75
|
+
if (captures = @template_url.match(%r{\As3://(?<bucket>[^/]+)/(?<key>.+)\z}))
|
76
|
+
bucket = captures[:bucket]
|
77
|
+
bucket = bucket.gsub('${AWS_REGION}', @region)
|
78
|
+
key = captures[:key]
|
79
|
+
key = key.gsub('${TEMPLATE_NAME}', File.basename(stack_path))
|
80
|
+
key = key.gsub('${TIMESTAMP}', Time.now.to_i.to_s)
|
81
|
+
obj = @s3_client.bucket(bucket).object(key)
|
82
|
+
obj.put(body: template)
|
83
|
+
obj.public_url
|
84
|
+
else
|
85
|
+
raise ValidationError, format('Malformed template URL: %p', @template_url)
|
86
|
+
end
|
87
|
+
else
|
88
|
+
raise ValidationError, 'Unable to validate template: it is larger than 51200 bytes and no template URL has been configured'
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
def validate_parameter_use(template)
|
93
|
+
available = {}
|
94
|
+
template.fetch('Parameters', {}).each_key { |key| available[key] = true }
|
95
|
+
unused = available.dup
|
96
|
+
PSEUDO_PARAMETERS.each { |key| available[key] = true }
|
97
|
+
template.fetch('Resources', {}).each_key { |key| available[key] = true }
|
98
|
+
undefined = {}
|
99
|
+
each_ref(template) do |key|
|
100
|
+
unused.delete(key)
|
101
|
+
undefined[key] = true unless available[key]
|
102
|
+
end
|
103
|
+
unless unused.empty?
|
104
|
+
raise ValidationError, "Unused parameters: #{unused.keys.join(', ')}"
|
105
|
+
end
|
106
|
+
unless undefined.empty?
|
107
|
+
raise ValidationError, "Undefined parameters: #{undefined.keys.join(', ')}"
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
def load_top_level_file(name)
|
112
|
+
matches = Dir["#{name}.{json,yml,yaml}"]
|
113
|
+
case matches.size
|
114
|
+
when 1
|
115
|
+
load(matches.first)
|
116
|
+
when 0
|
117
|
+
nil
|
118
|
+
else
|
119
|
+
sprintf('Found multiple files when looking for %s: %s', name, matches.join(', '))
|
120
|
+
raise AmbiguityError, message
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
def load_resources
|
125
|
+
Dir['resources/**/*.{json,yml,yaml}'].sort.each_with_object({}) do |path, acc|
|
126
|
+
relative_path = path.sub(/^resources\//, '')
|
127
|
+
acc[relative_path] = load(path)
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
def load(path)
|
132
|
+
YAML.load_file(path)
|
133
|
+
rescue Psych::SyntaxError => e
|
134
|
+
raise ParseError, sprintf('Invalid template fragment: %s', e.message), e.backtrace
|
135
|
+
end
|
136
|
+
|
137
|
+
def compile_resources(resources)
|
138
|
+
compiled = {resources: {}}
|
139
|
+
resources.map do |relative_path, resource|
|
140
|
+
name = relative_path_to_name(relative_path)
|
141
|
+
if (type = resource['Type']).start_with?('Regentanz::Resources::')
|
142
|
+
expanded_template = resource_compiler(type).compile(name, resource)
|
143
|
+
expanded_template[:resources] = expand_refs(expanded_template[:resources])
|
144
|
+
compiled.merge!(expanded_template) { |_, v1, v2| v1.merge(v2) }
|
145
|
+
else
|
146
|
+
compiled[:resources][name] = expand_refs(resource)
|
147
|
+
end
|
148
|
+
end
|
149
|
+
compiled
|
150
|
+
end
|
151
|
+
|
152
|
+
def resource_compiler(type)
|
153
|
+
@resource_compilers[type] ||= begin
|
154
|
+
begin
|
155
|
+
create_instance(type)
|
156
|
+
rescue NameError
|
157
|
+
begin
|
158
|
+
load_resource_compiler(type)
|
159
|
+
create_instance(type)
|
160
|
+
rescue LoadError, NameError
|
161
|
+
raise Regentanz::Error, "No resource compiler for #{type}"
|
162
|
+
end
|
163
|
+
end
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
def create_instance(type)
|
168
|
+
type.split('::').reduce(Object, &:const_get).new
|
169
|
+
end
|
170
|
+
|
171
|
+
def load_resource_compiler(type)
|
172
|
+
require(type.gsub('::', '/').gsub(/\B[A-Z]/, '_\&').downcase)
|
173
|
+
end
|
174
|
+
|
175
|
+
def compile_parameters(specifications)
|
176
|
+
groups = []
|
177
|
+
parameters = {}
|
178
|
+
specifications.each do |name, options|
|
179
|
+
if options['Type'] == 'Regentanz::ParameterGroup'
|
180
|
+
group_parameters = options['Parameters']
|
181
|
+
parameters.merge!(group_parameters)
|
182
|
+
groups << {
|
183
|
+
'Label' => {'default' => name},
|
184
|
+
'Parameters' => group_parameters.keys
|
185
|
+
}
|
186
|
+
else
|
187
|
+
parameters[name] = options
|
188
|
+
end
|
189
|
+
end
|
190
|
+
labels = parameters.each_with_object({}) do |(name, options), labels|
|
191
|
+
if (label = options.delete('Label'))
|
192
|
+
labels[name] = {'default' => label}
|
193
|
+
end
|
194
|
+
end
|
195
|
+
metadata = {'ParameterGroups' => groups, 'ParameterLabels' => labels}
|
196
|
+
return parameters, metadata
|
197
|
+
end
|
198
|
+
|
199
|
+
def relative_path_to_name(relative_path)
|
200
|
+
name = relative_path.dup
|
201
|
+
name.sub!(/\.([^.]+)$/, '')
|
202
|
+
name.gsub!('/', '_')
|
203
|
+
name.gsub!(/_.|^./) { |str| str[-1].upcase }
|
204
|
+
name
|
205
|
+
end
|
206
|
+
|
207
|
+
def each_ref(resource, &block)
|
208
|
+
case resource
|
209
|
+
when Hash
|
210
|
+
if (ref = resource['Ref'])
|
211
|
+
yield ref
|
212
|
+
elsif (substitution = resource['Fn::Sub'])
|
213
|
+
case substitution
|
214
|
+
when Array
|
215
|
+
each_ref(substitution, &block)
|
216
|
+
else
|
217
|
+
substitution.scan(/\$\{([^}]+)\}/) do |matches|
|
218
|
+
block.call(matches[0])
|
219
|
+
end
|
220
|
+
end
|
221
|
+
else
|
222
|
+
resource.each_value { |v| each_ref(v, &block) }
|
223
|
+
end
|
224
|
+
when Array
|
225
|
+
resource.each { |v| each_ref(v, &block) }
|
226
|
+
end
|
227
|
+
end
|
228
|
+
|
229
|
+
def expand_refs(resource)
|
230
|
+
case resource
|
231
|
+
when Hash
|
232
|
+
if (reference = resource['ResolveRef'])
|
233
|
+
expanded_name = relative_path_to_name(reference)
|
234
|
+
expanded_resource = resource.merge('Ref' => expanded_name)
|
235
|
+
expanded_resource.delete('ResolveRef')
|
236
|
+
expanded_resource
|
237
|
+
elsif (reference = resource['ResolveName'])
|
238
|
+
relative_path_to_name(reference)
|
239
|
+
elsif (reference = resource['Regentanz::ReadFile'])
|
240
|
+
read_file(reference)
|
241
|
+
else
|
242
|
+
resource.merge(resource) do |_, v, _|
|
243
|
+
expand_refs(v)
|
244
|
+
end
|
245
|
+
end
|
246
|
+
when Array
|
247
|
+
resource.map do |v|
|
248
|
+
expand_refs(v)
|
249
|
+
end
|
250
|
+
else
|
251
|
+
resource
|
252
|
+
end
|
253
|
+
end
|
254
|
+
|
255
|
+
def read_file(filename)
|
256
|
+
if File.exists?(filename)
|
257
|
+
File.read(filename)
|
258
|
+
else
|
259
|
+
raise ParseError, "File #{filename} does not exist"
|
260
|
+
end
|
261
|
+
end
|
262
|
+
end
|
263
|
+
end
|
data/lib/regentanz/version.rb
CHANGED
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require 'yaml'
|
3
|
+
|
4
|
+
module YAML
|
5
|
+
add_domain_type('regentanz', 'GetAtt') do |_, value|
|
6
|
+
{'Fn::GetAtt' => value.to_s.split('.')}
|
7
|
+
end
|
8
|
+
%w[Ref ResolveRef].each do |name|
|
9
|
+
add_domain_type('regentanz', name) do |tag, value|
|
10
|
+
{name => value}
|
11
|
+
end
|
12
|
+
end
|
13
|
+
%w[FindInMap GetAZs ImportValue Join Select Split Sub].each do |name|
|
14
|
+
add_domain_type('regentanz', name) do |tag, value|
|
15
|
+
{['Fn', name].join('::') => value}
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,692 @@
|
|
1
|
+
require 'tmpdir'
|
2
|
+
|
3
|
+
module Regentanz
|
4
|
+
module Resources
|
5
|
+
module Test
|
6
|
+
class SecurityGroupPair
|
7
|
+
def compile(name, resource)
|
8
|
+
{
|
9
|
+
:resources => {
|
10
|
+
"#{name}1" => resource.merge('Type' => 'AWS::EC2::SecurityGroup'),
|
11
|
+
"#{name}2" => resource.merge('Type' => 'AWS::EC2::SecurityGroup'),
|
12
|
+
}
|
13
|
+
}
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
class BucketAndPolicy
|
18
|
+
def compile(name, resource)
|
19
|
+
result = {:parameters => {}, :resources => {}, :mappings => {}, :conditions => {}}
|
20
|
+
result[:parameters]['BucketName'] = {'Type' => 'String'}
|
21
|
+
result[:resources][name + 'Bucket'] = {'Type' => 'AWS::S3::Bucket', 'Properties' => {'BucketName' => {'Ref' => 'BucketName'}}}
|
22
|
+
result[:resources][name + 'Policy'] = {'Type' => 'AWS::IAM::Policy', 'Properties' => {'PolicyName' => 'SomePolicy'}}
|
23
|
+
result[:mappings]['Prefix'] = {'production' => 'prod-', 'test' => 'test-'}
|
24
|
+
result[:conditions]['IsEu'] = {'Fn::Equals' => [{'Ref' => 'AWS::Region'}, 'eu-west-1']}
|
25
|
+
result
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
describe TemplateCompiler do
|
32
|
+
let :compiler do
|
33
|
+
described_class.new(config, cloud_formation_client: cf_client, s3_client: s3_client)
|
34
|
+
end
|
35
|
+
|
36
|
+
let :config do
|
37
|
+
{
|
38
|
+
'default_region' => 'ap-southeast-1',
|
39
|
+
}
|
40
|
+
end
|
41
|
+
|
42
|
+
let :cf_client do
|
43
|
+
double(:cf_client)
|
44
|
+
end
|
45
|
+
|
46
|
+
let :s3_client do
|
47
|
+
double(:s3)
|
48
|
+
end
|
49
|
+
|
50
|
+
before do
|
51
|
+
allow(cf_client).to receive(:validate_template)
|
52
|
+
end
|
53
|
+
|
54
|
+
describe '#compile_template' do
|
55
|
+
let :resources do
|
56
|
+
{
|
57
|
+
'core/ec2_instance.json' => {
|
58
|
+
'Type' => 'AWS::EC2::Instance',
|
59
|
+
'Properties' => {
|
60
|
+
'ImageId' => {'Fn::FindInMap' => ['Ami', 'amzn-ami-2016.03.1', 'hvm']},
|
61
|
+
}
|
62
|
+
},
|
63
|
+
'core/asg.json' => {
|
64
|
+
'Type' => 'AWS::AutoScaling::AutoScalingGroup',
|
65
|
+
'Properties' => {
|
66
|
+
'MinSize' => {'Ref' => 'MinInstances'},
|
67
|
+
'MaxSize' => {'Ref' => 'MaxInstances'},
|
68
|
+
}
|
69
|
+
}
|
70
|
+
}
|
71
|
+
end
|
72
|
+
|
73
|
+
let :parameters do
|
74
|
+
{
|
75
|
+
'MinInstances' => {
|
76
|
+
'Type' => 'Number',
|
77
|
+
'Default' => 1,
|
78
|
+
},
|
79
|
+
'MaxInstances' => {
|
80
|
+
'Type' => 'Number',
|
81
|
+
'Default' => 1,
|
82
|
+
},
|
83
|
+
'Environment' => {
|
84
|
+
'Type' => 'String',
|
85
|
+
},
|
86
|
+
'Volume' => {
|
87
|
+
'Type' => 'AWS::EC2::Volume',
|
88
|
+
},
|
89
|
+
}
|
90
|
+
end
|
91
|
+
|
92
|
+
let :mappings do
|
93
|
+
{
|
94
|
+
'Version' => {
|
95
|
+
'Mongo' => {
|
96
|
+
'production' => '2.6.0',
|
97
|
+
'staging' => '3.4.0',
|
98
|
+
},
|
99
|
+
},
|
100
|
+
}
|
101
|
+
end
|
102
|
+
|
103
|
+
let :conditions do
|
104
|
+
{
|
105
|
+
"Staging" => {"Fn::Equals" => [{"Ref" => "Environment"}, "staging"]},
|
106
|
+
}
|
107
|
+
end
|
108
|
+
|
109
|
+
let :outputs do
|
110
|
+
{
|
111
|
+
"VolumeId" => {"Value" => { "Ref" => "Volume" }},
|
112
|
+
}
|
113
|
+
end
|
114
|
+
|
115
|
+
let :template do
|
116
|
+
compiler.compile_template(resources, parameters: parameters, mappings: mappings, conditions: conditions, outputs: outputs)
|
117
|
+
end
|
118
|
+
|
119
|
+
it 'generates an CloudFormation compatible template', aggregate_failures: true do
|
120
|
+
expect(template['AWSTemplateFormatVersion']).to eq('2010-09-09')
|
121
|
+
expect(template['Mappings']).to eq(mappings)
|
122
|
+
expect(template['Conditions']).to eq(conditions)
|
123
|
+
expect(template['Outputs']).to eq(outputs)
|
124
|
+
expect(template['Resources']).to be_a(Hash)
|
125
|
+
expect(template['Resources'].values).to eq(resources.values)
|
126
|
+
end
|
127
|
+
|
128
|
+
it 'names resources from their relative paths' do
|
129
|
+
expect(template['Resources'].keys).to eq(%w[CoreEc2Instance CoreAsg])
|
130
|
+
end
|
131
|
+
|
132
|
+
it 'resolves references in resource definitions' do
|
133
|
+
resources['extra'] = {
|
134
|
+
'Type' => 'AWS::EC2::VolumeAttachment',
|
135
|
+
'Properties' => {
|
136
|
+
'InstanceId' => { 'ResolveRef' => 'core/ec2_instance' },
|
137
|
+
'VolumeId' => { 'Ref' => 'Volume' },
|
138
|
+
'Device' => '/dev/sdh',
|
139
|
+
}
|
140
|
+
}
|
141
|
+
expect(template['Resources']['Extra']).to eq(
|
142
|
+
'Type' => 'AWS::EC2::VolumeAttachment',
|
143
|
+
'Properties' => {
|
144
|
+
'InstanceId' => { 'Ref' => 'CoreEc2Instance' },
|
145
|
+
'VolumeId' => { 'Ref' => 'Volume' },
|
146
|
+
'Device' => '/dev/sdh',
|
147
|
+
}
|
148
|
+
)
|
149
|
+
end
|
150
|
+
|
151
|
+
it 'resolves reference names in resource definitions' do
|
152
|
+
resources['extra'] = {
|
153
|
+
'Type' => 'AWS::EC2::Instance',
|
154
|
+
'Properties' => {
|
155
|
+
'AvailabilityZone' => {'Fn::GetAtt' => [{'ResolveName' => 'core/ec2_instance'}]},
|
156
|
+
}
|
157
|
+
}
|
158
|
+
expect(template['Resources']['Extra']).to eq(
|
159
|
+
'Type' => 'AWS::EC2::Instance',
|
160
|
+
'Properties' => {
|
161
|
+
'AvailabilityZone' => {'Fn::GetAtt' => ['CoreEc2Instance']},
|
162
|
+
}
|
163
|
+
)
|
164
|
+
end
|
165
|
+
|
166
|
+
it 'reads external files in resource definitions' do
|
167
|
+
Dir.mktmpdir do |dir|
|
168
|
+
Dir.chdir(dir) do
|
169
|
+
File.open('code.py', 'w') do |file|
|
170
|
+
file.puts 'Some code 1'
|
171
|
+
file.puts 'Some code 2'
|
172
|
+
end
|
173
|
+
|
174
|
+
resources['extra'] = {
|
175
|
+
'Type' => 'AWS::Lambda::Function',
|
176
|
+
'Properties' => {
|
177
|
+
'Code' => {
|
178
|
+
'ZipFile' => {'Regentanz::ReadFile' => 'code.py'}
|
179
|
+
}
|
180
|
+
}
|
181
|
+
}
|
182
|
+
|
183
|
+
template = compiler.compile_template(resources, parameters: parameters, mappings: mappings, conditions: conditions, outputs: outputs, stack_path: dir)
|
184
|
+
|
185
|
+
expect(template['Resources']['Extra']).to eq(
|
186
|
+
'Type' => 'AWS::Lambda::Function',
|
187
|
+
'Properties' => {
|
188
|
+
'Code' => {
|
189
|
+
'ZipFile' => "Some code 1\nSome code 2\n"
|
190
|
+
}
|
191
|
+
}
|
192
|
+
)
|
193
|
+
end
|
194
|
+
end
|
195
|
+
end
|
196
|
+
|
197
|
+
context 'with parameter labels' do
|
198
|
+
it 'removes the Label key from parameters' do
|
199
|
+
parameters['MinInstances']['Label'] = 'The X'
|
200
|
+
expect(template['Parameters']['MinInstances']).not_to include('Label')
|
201
|
+
end
|
202
|
+
|
203
|
+
it 'adds parameter labels to the interface metadata' do
|
204
|
+
parameters['MinInstances']['Label'] = 'The minimum number of instances'
|
205
|
+
expect(template['Metadata']['AWS::CloudFormation::Interface']['ParameterLabels']).to eq('MinInstances' => {'default' => 'The minimum number of instances'})
|
206
|
+
end
|
207
|
+
end
|
208
|
+
|
209
|
+
context 'with Regentanz::ParameterGroup' do
|
210
|
+
let :parameters do
|
211
|
+
super().merge(
|
212
|
+
'Group' => {
|
213
|
+
'Type' => 'Regentanz::ParameterGroup',
|
214
|
+
'Parameters' => {
|
215
|
+
'Nested' => {
|
216
|
+
'Type' => 'AWS::EC2::Instance',
|
217
|
+
'Properties' => {
|
218
|
+
'AvailabilityZone' => {'Fn::GetAtt' => ['CoreEc2Instance']},
|
219
|
+
}
|
220
|
+
},
|
221
|
+
}
|
222
|
+
}
|
223
|
+
)
|
224
|
+
end
|
225
|
+
|
226
|
+
let :resources do
|
227
|
+
r = super()
|
228
|
+
r['core/asg.json']['Properties']['AvailabilityZones'] = {'Ref' => 'Nested'}
|
229
|
+
r
|
230
|
+
end
|
231
|
+
|
232
|
+
it 'removes the group parameters from the output' do
|
233
|
+
expect(template['Parameters']).not_to include('Group')
|
234
|
+
end
|
235
|
+
|
236
|
+
it 'lifts the grouped parameters to the top level' do
|
237
|
+
expect(template['Parameters'].keys).to include('Nested')
|
238
|
+
end
|
239
|
+
|
240
|
+
it 'adds parameter groups to the interface metadata' do
|
241
|
+
expect(template['Metadata']['AWS::CloudFormation::Interface']['ParameterGroups']).to eq([{'Label' => {'default' => 'Group'}, 'Parameters' => ['Nested']}])
|
242
|
+
end
|
243
|
+
end
|
244
|
+
|
245
|
+
context 'with a custom resource type' do
|
246
|
+
let :resources do
|
247
|
+
super().merge(
|
248
|
+
'core/test.json' => {
|
249
|
+
'Type' => 'Regentanz::Resources::Test::BucketAndPolicy',
|
250
|
+
'Properties' => {
|
251
|
+
'Name' => {'Ref' => 'CoreTestBucketName'},
|
252
|
+
}
|
253
|
+
},
|
254
|
+
'core/lc.json' => {
|
255
|
+
'Type' => 'AWS::AutoScaling::LaunchConfiguration',
|
256
|
+
'Properties' => {
|
257
|
+
'SecurityGroups' => [{'Ref' => 'AWS::NoValue'}]
|
258
|
+
}
|
259
|
+
},
|
260
|
+
)
|
261
|
+
end
|
262
|
+
|
263
|
+
it 'does not add the uncompiled resource' do
|
264
|
+
expect(template['Resources']).not_to include('CoreTest')
|
265
|
+
end
|
266
|
+
|
267
|
+
it 'adds resources from the compiled resource' do
|
268
|
+
expect(template['Resources']).to include('CoreTestBucket', 'CoreTestPolicy')
|
269
|
+
end
|
270
|
+
|
271
|
+
it 'keeps existing resources' do
|
272
|
+
expect(template['Resources']).to include('CoreEc2Instance')
|
273
|
+
end
|
274
|
+
|
275
|
+
it 'adds parameters from the compiled resource' do
|
276
|
+
expect(template['Parameters']).to include('BucketName')
|
277
|
+
end
|
278
|
+
|
279
|
+
it 'keeps parameters from outside the resource' do
|
280
|
+
expect(template['Parameters']).to include('MinInstances')
|
281
|
+
end
|
282
|
+
|
283
|
+
it 'adds mappings from the compiled resource' do
|
284
|
+
expect(template['Mappings']).to include('Prefix')
|
285
|
+
end
|
286
|
+
|
287
|
+
it 'keeps parameters from outside the resource' do
|
288
|
+
expect(template['Mappings']).to include('Version')
|
289
|
+
end
|
290
|
+
|
291
|
+
it 'adds conditions from the compiled resource' do
|
292
|
+
expect(template['Conditions']).to include('IsEu')
|
293
|
+
end
|
294
|
+
|
295
|
+
it 'keeps conditions from outside the resource' do
|
296
|
+
expect(template['Conditions']).to include('Staging')
|
297
|
+
end
|
298
|
+
|
299
|
+
context 'that contains a reference to another resource' do
|
300
|
+
let :resources do
|
301
|
+
r = super().merge(
|
302
|
+
'core/sg.json' => {
|
303
|
+
'Type' => 'Regentanz::Resources::Test::SecurityGroupPair',
|
304
|
+
'Properties' => {
|
305
|
+
'GroupDescription' => {'Fn::Join' => [' ', 'SG for ', {'ResolveName' => 'core/vpc'}]},
|
306
|
+
'VpcId' => {'ResolveRef' => 'core/vpc'},
|
307
|
+
}
|
308
|
+
},
|
309
|
+
'core/vpc.json' => {
|
310
|
+
'Type' => 'AWS::EC2::VPC',
|
311
|
+
'Properties' => {
|
312
|
+
'CidrBlock' => '10.0.0.0/8',
|
313
|
+
}
|
314
|
+
},
|
315
|
+
'core/lc.json' => {
|
316
|
+
'Type' => 'AWS::AutoScaling::LaunchConfiguration',
|
317
|
+
'Properties' => {
|
318
|
+
'SecurityGroups' => [{'Ref' => 'AWS::NoValue'}]
|
319
|
+
}
|
320
|
+
},
|
321
|
+
)
|
322
|
+
end
|
323
|
+
|
324
|
+
it 'resolves references in the resources returned by the custom resource' do
|
325
|
+
aggregate_failures do
|
326
|
+
expect(template['Resources']['CoreSg1']['Properties']['VpcId']).to eq({'Ref' => 'CoreVpc'})
|
327
|
+
expect(template['Resources']['CoreSg1']['Properties']['GroupDescription']).to eq({'Fn::Join' => [' ', 'SG for ', 'CoreVpc']})
|
328
|
+
end
|
329
|
+
end
|
330
|
+
|
331
|
+
it 'does not mutate the input template' do
|
332
|
+
aggregate_failures do
|
333
|
+
expect(template['Resources']['CoreSg2']['Properties']['VpcId']).to eq({'Ref' => 'CoreVpc'})
|
334
|
+
expect(template['Resources']['CoreSg2']['Properties']['GroupDescription']).to eq({'Fn::Join' => [' ', 'SG for ', 'CoreVpc']})
|
335
|
+
end
|
336
|
+
end
|
337
|
+
end
|
338
|
+
|
339
|
+
context 'with nil valued options' do
|
340
|
+
let :parameters do
|
341
|
+
nil
|
342
|
+
end
|
343
|
+
|
344
|
+
let :conditions do
|
345
|
+
nil
|
346
|
+
end
|
347
|
+
|
348
|
+
let :mappings do
|
349
|
+
nil
|
350
|
+
end
|
351
|
+
|
352
|
+
let :outputs do
|
353
|
+
nil
|
354
|
+
end
|
355
|
+
|
356
|
+
before do
|
357
|
+
resources.delete_if { |k,v| k != 'core/test.json' }
|
358
|
+
end
|
359
|
+
|
360
|
+
it 'includes the values from the compiled resource', aggregate_failures: true do
|
361
|
+
expect(template['Resources']).to include('CoreTestBucket', 'CoreTestPolicy')
|
362
|
+
expect(template['Parameters']).to include('BucketName')
|
363
|
+
expect(template['Conditions']).to include('IsEu')
|
364
|
+
expect(template['Mappings']).to include('Prefix')
|
365
|
+
expect(template).not_to include('Outputs')
|
366
|
+
end
|
367
|
+
end
|
368
|
+
|
369
|
+
context 'when the resource is not loaded' do
|
370
|
+
let :resources do
|
371
|
+
super().merge(
|
372
|
+
'core/unloaded_resource.json' => {
|
373
|
+
'Type' => 'Regentanz::Resources::Test::Unloaded',
|
374
|
+
}
|
375
|
+
)
|
376
|
+
end
|
377
|
+
|
378
|
+
it 'converts the resource type to a path and loads it' do
|
379
|
+
expect(template['Resources']).to include('CoreUnloadedResource')
|
380
|
+
end
|
381
|
+
end
|
382
|
+
end
|
383
|
+
|
384
|
+
context 'with an non-existant resource type' do
|
385
|
+
let :resources do
|
386
|
+
super().merge(
|
387
|
+
'core/test.json' => {
|
388
|
+
'Type' => 'Regentanz::Resources::NonexistantResource',
|
389
|
+
'Properties' => {
|
390
|
+
'Role' => 'core-role',
|
391
|
+
}
|
392
|
+
}
|
393
|
+
)
|
394
|
+
end
|
395
|
+
|
396
|
+
it 'raises an error' do
|
397
|
+
expect { template }.to raise_error(Regentanz::Error, 'No resource compiler for Regentanz::Resources::NonexistantResource')
|
398
|
+
end
|
399
|
+
end
|
400
|
+
|
401
|
+
context 'when validating that parameters are used' do
|
402
|
+
let :parameters do
|
403
|
+
super().merge(
|
404
|
+
'Foo' => {'Type' => 'String'},
|
405
|
+
'Bar' => {'Type' => 'String'},
|
406
|
+
'Baz' => {'Type' => 'String'},
|
407
|
+
)
|
408
|
+
end
|
409
|
+
|
410
|
+
context 'and there are unused parameters' do
|
411
|
+
it 'raises ValidationError' do
|
412
|
+
expect { template }.to raise_error(described_class::ValidationError, 'Unused parameters: Foo, Bar, Baz')
|
413
|
+
end
|
414
|
+
end
|
415
|
+
|
416
|
+
context 'and parameters are used in Fn::Sub' do
|
417
|
+
let :resources do
|
418
|
+
super().merge(
|
419
|
+
'core/test.json' => {
|
420
|
+
'Type' => 'AWS::EC2::Instance',
|
421
|
+
'Properties' => {
|
422
|
+
'SomeProp' => {'Fn::Sub' => 'xyz:${Foo}/bar/${Baz}'},
|
423
|
+
'SomeOtherProp' => {'Fn::Sub' => ['${barbar}!', {'barbar' => {'Ref' => 'Bar'}}]},
|
424
|
+
}
|
425
|
+
}
|
426
|
+
)
|
427
|
+
end
|
428
|
+
|
429
|
+
it 'detects the usage' do
|
430
|
+
expect { template }.to_not raise_error
|
431
|
+
end
|
432
|
+
end
|
433
|
+
end
|
434
|
+
|
435
|
+
context 'when validating that no undefined parameters are used' do
|
436
|
+
context 'and parameters are used in Fn::Sub' do
|
437
|
+
let :resources do
|
438
|
+
super().merge(
|
439
|
+
'core/test.json' => {
|
440
|
+
'Type' => 'AWS::EC2::Instance',
|
441
|
+
'Properties' => {
|
442
|
+
'SomeProp' => {'Fn::Sub' => 'xyz:${Foo}/bar/${Baz}'},
|
443
|
+
'SomeOtherProp' => {'Fn::Sub' => ['${barbar}!', {'barbar' => {'Ref' => 'Bar'}}]},
|
444
|
+
}
|
445
|
+
}
|
446
|
+
)
|
447
|
+
end
|
448
|
+
|
449
|
+
it 'raises ValidationError' do
|
450
|
+
expect { template }.to raise_error(described_class::ValidationError, 'Undefined parameters: Foo, Baz, Bar')
|
451
|
+
end
|
452
|
+
end
|
453
|
+
|
454
|
+
context 'and referring to other resources' do
|
455
|
+
let :resources do
|
456
|
+
super().merge(
|
457
|
+
'core/test.json' => {
|
458
|
+
'Type' => 'AWS::EC2::Instance',
|
459
|
+
'Properties' => {
|
460
|
+
'SomeProp' => {'ResolveRef' => 'core/test2'}
|
461
|
+
}
|
462
|
+
},
|
463
|
+
)
|
464
|
+
end
|
465
|
+
|
466
|
+
it 'raises ValidationError' do
|
467
|
+
expect { template }.to raise_error(described_class::ValidationError, 'Undefined parameters: CoreTest2')
|
468
|
+
end
|
469
|
+
|
470
|
+
context 'when resource exists' do
|
471
|
+
let :resources do
|
472
|
+
super().merge(
|
473
|
+
'core/test2.json' => {
|
474
|
+
'Type' => 'AWS::EC2::Instance',
|
475
|
+
'Properties' => {
|
476
|
+
}
|
477
|
+
},
|
478
|
+
)
|
479
|
+
end
|
480
|
+
|
481
|
+
it 'accepts the parameter' do
|
482
|
+
expect { template }.not_to raise_error
|
483
|
+
end
|
484
|
+
end
|
485
|
+
end
|
486
|
+
|
487
|
+
context 'and using built-in pseudo parameters' do
|
488
|
+
let :resources do
|
489
|
+
super().merge(
|
490
|
+
'core/test.json' => {
|
491
|
+
'Type' => 'AWS::EC2::Instance',
|
492
|
+
'Properties' => {
|
493
|
+
'SomeProp' => {'Ref' => 'AWS::Region'}
|
494
|
+
}
|
495
|
+
}
|
496
|
+
)
|
497
|
+
end
|
498
|
+
|
499
|
+
it 'accepts the parameter' do
|
500
|
+
expect { template }.not_to raise_error
|
501
|
+
end
|
502
|
+
end
|
503
|
+
end
|
504
|
+
end
|
505
|
+
|
506
|
+
describe '#compile_from_path' do
|
507
|
+
around do |example|
|
508
|
+
Dir.mktmpdir do |dir|
|
509
|
+
Dir.chdir(dir) do
|
510
|
+
Dir.mkdir('template')
|
511
|
+
example.call
|
512
|
+
end
|
513
|
+
end
|
514
|
+
end
|
515
|
+
|
516
|
+
let :template do
|
517
|
+
compiler.compile_from_path('template')
|
518
|
+
end
|
519
|
+
|
520
|
+
it 'loads parameters, mappings, conditions, outputs from JSON files', aggregate_failures: true do
|
521
|
+
File.write('template/parameters.json', '{"Parameter":{"Type":"Number"}}')
|
522
|
+
File.write('template/mappings.json', '{"Mapping":{"A":{"B":"C"}}}')
|
523
|
+
File.write('template/conditions.json', '{"Staging":{"Fn:Equals":[{"Ref":"AWS::Region"},{"Ref":"Parameter"}]}}')
|
524
|
+
File.write('template/outputs.json', '{"VolumeId":{"Value":{"Ref":"Parameter"}}}')
|
525
|
+
expect(template['Parameters'].keys).to eq(%w[Parameter])
|
526
|
+
expect(template['Mappings'].keys).to eq(%w[Mapping])
|
527
|
+
expect(template['Conditions'].keys).to eq(%w[Staging])
|
528
|
+
expect(template['Outputs'].keys).to eq(%w[VolumeId])
|
529
|
+
end
|
530
|
+
|
531
|
+
it 'loads parameters, mappings, conditions, outputs from YAML files', aggregate_failures: true do
|
532
|
+
File.write('template/parameters.yaml', 'Parameter: {Type: Number}')
|
533
|
+
File.write('template/mappings.yaml', 'Mapping: {A: {B: C}}')
|
534
|
+
File.write('template/conditions.yml', 'Staging: {"Fn:Equals": [{Ref: "AWS::Region"}, {Ref: Parameter}]}')
|
535
|
+
File.write('template/outputs.yml', 'VolumeId: {Value: {Ref: Parameter}}')
|
536
|
+
expect(template['Parameters'].keys).to eq(%w[Parameter])
|
537
|
+
expect(template['Mappings'].keys).to eq(%w[Mapping])
|
538
|
+
expect(template['Conditions'].keys).to eq(%w[Staging])
|
539
|
+
expect(template['Outputs'].keys).to eq(%w[VolumeId])
|
540
|
+
end
|
541
|
+
|
542
|
+
it 'loads resource from JSON files' do
|
543
|
+
Dir.mkdir 'template/resources'
|
544
|
+
Dir.mkdir 'template/resources/core'
|
545
|
+
File.write('template/resources/core/instance.json', '{"Type":"AWS::EC2::Instance"}')
|
546
|
+
File.write('template/resources/attachment.json', '{"Type":"AWS::EC2::VolumeAttachment"}')
|
547
|
+
expect(template['Resources'].keys.sort).to eq(%w[Attachment CoreInstance])
|
548
|
+
end
|
549
|
+
|
550
|
+
it 'loads resource from YAML files' do
|
551
|
+
Dir.mkdir 'template/resources'
|
552
|
+
Dir.mkdir 'template/resources/core'
|
553
|
+
File.write('template/resources/core/instance.yaml', 'Type: AWS::EC2::Instance')
|
554
|
+
File.write('template/resources/attachment.yml', 'Type: AWS::EC2::VolumeAttachment')
|
555
|
+
expect(template['Resources'].keys.sort).to eq(%w[Attachment CoreInstance])
|
556
|
+
end
|
557
|
+
|
558
|
+
it 'handles short-form intrinsic functions in YAML' do
|
559
|
+
Dir.mkdir 'template/resources'
|
560
|
+
Dir.mkdir 'template/resources/core'
|
561
|
+
File.write('template/resources/core/instance.json', '{"Type":"AWS::EC2::Instance"}')
|
562
|
+
File.write('template/resources/volume.yml', <<-'YAML')
|
563
|
+
Type: AWS::EC2::Volume,
|
564
|
+
Properties:
|
565
|
+
Size: !ImportValue VolumeSize
|
566
|
+
AvailabilityZone: !GetAtt Instance.AvailabilityZone
|
567
|
+
Tags:
|
568
|
+
- Key: MyTag
|
569
|
+
Value: !Sub "${Volume}-${Volume}"
|
570
|
+
YAML
|
571
|
+
File.write('template/resources/attachment.yml', <<-'YAML')
|
572
|
+
Type: AWS::EC2::VolumeAttachment
|
573
|
+
Properties:
|
574
|
+
InstanceId: !ResolveRef core/instance
|
575
|
+
VolumeId: !Ref Volume
|
576
|
+
Device: !Join [/, ['', dev, sdh]]
|
577
|
+
YAML
|
578
|
+
expect(template['Resources']['Volume']['Properties']).to eq(
|
579
|
+
'Size' => {'Fn::ImportValue' => 'VolumeSize'},
|
580
|
+
'AvailabilityZone' => {'Fn::GetAtt' => ['Instance', 'AvailabilityZone']},
|
581
|
+
'Tags' => [{'Key' => 'MyTag', 'Value' => {'Fn::Sub' => '${Volume}-${Volume}'}}],
|
582
|
+
)
|
583
|
+
expect(template['Resources']['Attachment']['Properties']).to eq(
|
584
|
+
'InstanceId' => {'Ref' => 'CoreInstance'},
|
585
|
+
'VolumeId' => {'Ref' => 'Volume'},
|
586
|
+
'Device' => {'Fn::Join' => ['/', ['', 'dev', 'sdh']]},
|
587
|
+
)
|
588
|
+
end
|
589
|
+
end
|
590
|
+
|
591
|
+
describe '#validate_template' do
|
592
|
+
it 'uses the CloudFormation API to validate the template' do
|
593
|
+
compiler.validate_template('stack', 'my-template')
|
594
|
+
expect(cf_client).to have_received(:validate_template).with(template_body: 'my-template')
|
595
|
+
end
|
596
|
+
|
597
|
+
it 'produces a validation error when template is invalid' do
|
598
|
+
allow(cf_client).to receive(:validate_template).and_raise(Aws::CloudFormation::Errors::ValidationError.new(nil, 'boork'))
|
599
|
+
expect { compiler.validate_template('stack', 'my-template') }.to raise_error(described_class::ValidationError, 'Invalid template: boork')
|
600
|
+
end
|
601
|
+
|
602
|
+
context 'when the compiled template is larger than 51200 bytes' do
|
603
|
+
let :config do
|
604
|
+
super().merge('template_url' => 's3://templates/validate-me.json')
|
605
|
+
end
|
606
|
+
|
607
|
+
let :large_template do
|
608
|
+
'0' * 51201
|
609
|
+
end
|
610
|
+
|
611
|
+
let :bucket do
|
612
|
+
double(:bucket)
|
613
|
+
end
|
614
|
+
|
615
|
+
let :s3_obj do
|
616
|
+
double(:s3_obj)
|
617
|
+
end
|
618
|
+
|
619
|
+
before do
|
620
|
+
allow(s3_client).to receive(:bucket).with('templates').and_return(bucket)
|
621
|
+
allow(bucket).to receive(:object).with('validate-me.json').and_return(s3_obj)
|
622
|
+
allow(s3_obj).to receive(:put)
|
623
|
+
allow(s3_obj).to receive(:public_url).and_return('https://s3.amazonaws.com/templates/validate-me.json')
|
624
|
+
end
|
625
|
+
|
626
|
+
it 'uploads the template to S3 before validating the template' do
|
627
|
+
compiler.validate_template('stack', large_template)
|
628
|
+
expect(s3_obj).to have_received(:put).with(body: large_template).ordered
|
629
|
+
expect(cf_client).to have_received(:validate_template).with(template_url: 'https://s3.amazonaws.com/templates/validate-me.json').ordered
|
630
|
+
end
|
631
|
+
|
632
|
+
context 'and the template URL contains variables' do
|
633
|
+
let :config do
|
634
|
+
super().merge('template_url' => 's3://templates-${AWS_REGION}/some/prefix/${TEMPLATE_NAME}/${TEMPLATE_NAME}-${TIMESTAMP}.json')
|
635
|
+
end
|
636
|
+
|
637
|
+
before do
|
638
|
+
allow(s3_client).to receive(:bucket).and_return(bucket)
|
639
|
+
allow(bucket).to receive(:object).and_return(s3_obj)
|
640
|
+
end
|
641
|
+
|
642
|
+
it 'replaces ${AWS_REGION} in the bucket name with the configured AWS region' do
|
643
|
+
compiler.validate_template('stack', large_template)
|
644
|
+
expect(s3_client).to have_received(:bucket).with('templates-ap-southeast-1')
|
645
|
+
end
|
646
|
+
|
647
|
+
it 'replaces ${TEMPLATE_NAME} in the key with the directory name of the template' do
|
648
|
+
compiler.validate_template('some/path/to/a/template/called/foobar', large_template)
|
649
|
+
expect(bucket).to have_received(:object).with(start_with('some/prefix/foobar/foobar-'))
|
650
|
+
end
|
651
|
+
|
652
|
+
it 'replaces ${TIMESTAMP} in the key with current time as a UNIX timestamp' do
|
653
|
+
compiler.validate_template('some/path/to/a/template/called/foobar', large_template)
|
654
|
+
expect(bucket).to have_received(:object).with(/-\d+\.json$/)
|
655
|
+
end
|
656
|
+
end
|
657
|
+
|
658
|
+
context 'and the template URL is malformed' do
|
659
|
+
let :config do
|
660
|
+
super().merge('template_url' => 's5://templates-${AWS_REGION}/some/prefix/${TEMPLATE_NAME}-${TIMESTAMP}.json')
|
661
|
+
end
|
662
|
+
|
663
|
+
it 'raises an error' do
|
664
|
+
expect { compiler.validate_template('stack', large_template) }.to raise_error(described_class::ValidationError, 'Malformed template URL: "s5://templates-${AWS_REGION}/some/prefix/${TEMPLATE_NAME}-${TIMESTAMP}.json"')
|
665
|
+
end
|
666
|
+
end
|
667
|
+
|
668
|
+
context 'and no template bucket has been specified' do
|
669
|
+
let :config do
|
670
|
+
super().tap do |c|
|
671
|
+
c.delete('template_url')
|
672
|
+
end
|
673
|
+
end
|
674
|
+
|
675
|
+
it 'raises an error' do
|
676
|
+
expect { compiler.validate_template('stack', large_template) }.to raise_error(described_class::ValidationError, 'Unable to validate template: it is larger than 51200 bytes and no template URL has been configured')
|
677
|
+
end
|
678
|
+
end
|
679
|
+
end
|
680
|
+
|
681
|
+
context 'when the compiled template is larger than 460800 bytes' do
|
682
|
+
let :large_template do
|
683
|
+
'0' * 460801
|
684
|
+
end
|
685
|
+
|
686
|
+
it 'raises a template error' do
|
687
|
+
expect { compiler.validate_template('stack', large_template) }.to raise_error(described_class::TemplateError, 'Compiled template is too large: 460801 bytes > 460800')
|
688
|
+
end
|
689
|
+
end
|
690
|
+
end
|
691
|
+
end
|
692
|
+
end
|