cloudformation_rspec 0.0.9

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 9e741127a74f674f19e97e2d5c25dedfb96cab8eb471fe65a4ba8ba5d5e18fd4
4
+ data.tar.gz: 84004c6abe9054bdd0eccaf8a81996916976c29b45794fb591ef5ba098ffdbe2
5
+ SHA512:
6
+ metadata.gz: 7b0df6dabb6d58df4bc7bbbdfc70d9ced98dea284522ffc30eabf5fe31f8e15a0f39ba42ed18b293f1944af44a385e5b056370b367f5c4181c5f669b1f79e7a7
7
+ data.tar.gz: 98b37a583ed876cbd4fcf60ede9f642e7202010ea61ff23936ca319791bacefb70efb6000647d2c4ea2309e0158c3cfd4d0d3bc34365ab9ef7ca7ceba6633d61
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
@@ -0,0 +1,80 @@
1
+ # CloudFormation RSpec
2
+
3
+ CloudFormation provides a black-box orchestration model; you feed templates in and infrastructure pops out the other end. This is good in that we design our templates without having to understand how the sausage gets made, but we want a way to test, quickly, what it's actually doing is what we expect.
4
+
5
+ CloudFormation RSpec enables us to use RSpec matchers test your templates against the actual CloudFormation APIs without creating actual infrastructure. This is a tradeoff to get faster feedback (typically in under 2 minutes) without waiting for CloudFormation stacks to build and is designed to complement but not replace Acceptance Testing.
6
+
7
+ ## Usage
8
+
9
+ ### Testing template Change Set with Parameters
10
+
11
+ ```ruby
12
+ describe 'vpc_template' do
13
+ let(:template_json) { File.read('fixtures/vpc.json') }
14
+ let(:stack) {
15
+ template_body: template_json,
16
+ parameters: {
17
+ "VpcCidr" => cidr,
18
+ }
19
+ }
20
+
21
+ it 'is valid' do
22
+ expect(template_json).to be_valid
23
+ end
24
+
25
+ context 'with a valid cidr' do
26
+ let(:cidr) { "10.0.0.0/16" }
27
+
28
+ it 'creates a vpc' do
29
+ expect(stack).to contain_in_change_set("AWS::EC2::VPC", "vpc")
30
+ end
31
+ end
32
+
33
+ context 'with invalid cidr' do
34
+ let(:cidr) { "1.1.1.0/16" }
35
+
36
+ it 'fails to create a change set' do
37
+ expect(stack).to have_change_set_failed
38
+ end
39
+ end
40
+ end
41
+ ```
42
+
43
+ ### Testing SparkleFormation templates
44
+
45
+ ```ruby
46
+ describe 'vpc_template' do
47
+ let(:stack) {
48
+ compiler: :sparkleformation,
49
+ template_file: "templates/vpc.rb",
50
+ compile_state: {public_subnets: ["10.0.0.0/24", "10.0.1.0/24"], private_subnets: ["10.0.2.0/24", "10.0.3.0/24"]},
51
+ parameters: {
52
+ "VpcCidr" => cidr,
53
+ }
54
+ }
55
+
56
+ it 'is valid' do
57
+ expect(stack).to be_valid_sparkleformation
58
+ end
59
+
60
+ context 'with a valid cidr' do
61
+ let(:cidr) { "10.0.0.0/16" }
62
+
63
+ it 'creates a vpc' do
64
+ expect(stack).to contain_in_change_set("AWS::EC2::VPC", "vpc")
65
+ end
66
+ end
67
+
68
+ context 'with invalid cidr' do
69
+ let(:cidr) { "1.1.1.0/16" }
70
+
71
+ it 'fails to create a change set' do
72
+ expect(stack).to have_change_set_failed
73
+ end
74
+ end
75
+ end
76
+ ```
77
+
78
+ ## Limitations
79
+
80
+ Currently we don't support templates larger than 51,200 bytes, as this requires uploading the template to S3 first.
@@ -0,0 +1,5 @@
1
+ require 'rspec/core/rake_task'
2
+
3
+ RSpec::Core::RakeTask.new
4
+
5
+ task :default => [:spec]
@@ -0,0 +1,22 @@
1
+ $:.unshift File.expand_path("../lib", __FILE__)
2
+
3
+ Gem::Specification.new do |gem|
4
+ gem.name = "cloudformation_rspec"
5
+ gem.version = "0.0.9"
6
+
7
+ gem.authors = ["Patrick Robinson"]
8
+ gem.email = ["patrick.robinson@envato.com"]
9
+ gem.description = %q{CloudFormation RSpec matchers}
10
+ gem.summary = %q{Test your CloudFormation templates}
11
+ gem.homepage = "https://github.com/envato/cloudformation_rspec"
12
+
13
+ gem.files = `git ls-files`.split($\)
14
+ gem.executables = gem.files.grep(%r{^bin/}).map { |f| File.basename(f) }
15
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
16
+ gem.require_paths = ["lib"]
17
+
18
+ gem.add_dependency 'rspec', '~> 3'
19
+ gem.add_dependency 'sparkle_formation', '~> 3'
20
+ gem.add_dependency 'aws-sdk-cloudformation', '~> 1'
21
+ gem.add_development_dependency 'rake'
22
+ end
@@ -0,0 +1,7 @@
1
+ module CloudFormationRSpec
2
+ end
3
+
4
+ require 'cloudformation_rspec/resource_change'
5
+ require 'cloudformation_rspec/change_set'
6
+ require 'cloudformation_rspec/sparkle'
7
+ require 'cloudformation_rspec/matchers'
@@ -0,0 +1,99 @@
1
+ require 'aws-sdk-cloudformation'
2
+ require 'securerandom'
3
+ require 'digest'
4
+ require 'sparkle_formation'
5
+
6
+ class CloudFormationRSpec::ChangeSet
7
+ END_STATES = [
8
+ 'CREATE_COMPLETE',
9
+ 'DELETE_COMPLETE',
10
+ 'FAILED'
11
+ ]
12
+ WAIT_DELAY = 3
13
+
14
+ ChangeSetNotComplete = Class.new(StandardError)
15
+
16
+ @change_set_cache = {}
17
+
18
+ attr_reader :changes, :status
19
+
20
+ def initialize(template_body:, parameters: {})
21
+ @template_body = template_body
22
+ @parameters = parameters ? parameters : {}
23
+ end
24
+
25
+ def self.from_cloudformation_template(template_body:, parameters:)
26
+ new(template_body: template_body, parameters: parameters).tap { |change_set| change_set.create_change_set }
27
+ end
28
+
29
+ def self.from_sparkleformation_template(template_file:, compile_state:, parameters:)
30
+ template_body = CloudFormationRSpec::Sparkle.compile_sparkle_template(template_file, compile_state)
31
+
32
+ new(template_body: template_body, parameters: parameters).tap { |change_set| change_set.create_change_set }
33
+ end
34
+
35
+ def create_change_set
36
+ change_set_hash = generate_change_set_hash
37
+
38
+ if change_set = self.class.get_from_cache(change_set_hash)
39
+ @status = change_set.status
40
+ @changes = change_set.changes.map { |change| CloudFormationRSpec::ResourceChange.new(change.resource_change.resource_type, change.resource_change.logical_resource_id) }
41
+ return change_set
42
+ end
43
+
44
+ client = Aws::CloudFormation::Client.new
45
+ change_set = client.create_change_set(
46
+ change_set_name: change_set_name,
47
+ stack_name: change_set_name,
48
+ change_set_type: 'CREATE',
49
+ template_body: @template_body,
50
+ parameters: flat_parameters,
51
+ capabilities: ['CAPABILITY_IAM', 'CAPABILITY_NAMED_IAM'],
52
+ )
53
+ @change_set_id = change_set.id
54
+ stack_created = wait_change_set_review(client, change_set_name)
55
+ response = client.describe_change_set(change_set_name: @change_set_id, stack_name: change_set_name)
56
+ if !END_STATES.include? response.status
57
+ raise ChangeSetNotComplete.new("Change set did not complete in time. #{response.status}")
58
+ end
59
+ @status = response.status
60
+ @changes = response.changes.map { |change| CloudFormationRSpec::ResourceChange.new(change.resource_change.resource_type, change.resource_change.logical_resource_id) }
61
+ client.delete_change_set(change_set_name: change_set_name, stack_name: change_set_name)
62
+ client.delete_stack(stack_name: change_set_name) if stack_created
63
+ self.class.add_to_cache(change_set_hash, response)
64
+ response
65
+ end
66
+
67
+ private
68
+
69
+ def change_set_name
70
+ @change_set_name ||= "CloudFormationRSpec-#{SecureRandom.uuid}"
71
+ end
72
+
73
+ def flat_parameters
74
+ @parameters.map { |k, v| {parameter_key: k, parameter_value: v} }
75
+ end
76
+
77
+ def wait_change_set_review(client, change_set_name)
78
+ client.wait_until(:stack_exists, {stack_name: change_set_name}, {delay: WAIT_DELAY})
79
+ client.wait_until(:change_set_create_complete, {change_set_name: change_set_name, stack_name: change_set_name}, {delay: WAIT_DELAY})
80
+ rescue Aws::Waiters::Errors::WaiterFailed, Aws::Waiters::Errors::TooManyAttemptsError
81
+ false
82
+ end
83
+
84
+ def generate_change_set_hash
85
+ Digest::MD5.hexdigest(@template_body + @parameters.to_json)
86
+ end
87
+
88
+ def self.get_from_cache(change_set_hash)
89
+ @change_set_cache[change_set_hash]
90
+ end
91
+
92
+ def self.add_to_cache(change_set_hash, change_set)
93
+ @change_set_cache[change_set_hash] = change_set
94
+ end
95
+
96
+ def self.flush_cache
97
+ @change_set_cache = {}
98
+ end
99
+ end
@@ -0,0 +1,7 @@
1
+ module CloudFormationRSpec::Matchers
2
+ end
3
+
4
+ require 'rspec'
5
+ require 'cloudformation_rspec/matchers/validate'
6
+ require 'cloudformation_rspec/matchers/change_set'
7
+ require 'cloudformation_rspec/matchers/output'
@@ -0,0 +1,78 @@
1
+ require 'aws-sdk-cloudformation'
2
+
3
+ module CloudFormationRSpec::Matchers::ChangeSet
4
+ def generate_change_set(stack)
5
+ if stack[:compiler] == :sparkleformation
6
+ CloudFormationRSpec::ChangeSet.from_sparkleformation_template(
7
+ template_file: stack[:template_file],
8
+ compile_state: stack[:compile_state],
9
+ parameters: stack[:parameters]
10
+ )
11
+ else
12
+ if !stack[:template_body]
13
+ raise ArgumentError, "You must pass either :template_body or set :compiler and pass in compiler specific options"
14
+ end
15
+ CloudFormationRSpec::ChangeSet.from_cloudformation_template(template_body: stack[:template_body], parameters: stack[:parameters])
16
+ end
17
+ end
18
+
19
+ def resource_ids_with_resource_type(change_set_result, resource_type)
20
+ change_set_result.changes.select do |change|
21
+ change.resource_type == resource_type
22
+ end.map(&:logical_resource_id)
23
+ end
24
+ end
25
+
26
+ RSpec::Matchers.define :contain_in_change_set do |resource_type, resource_id|
27
+ include CloudFormationRSpec::Matchers::ChangeSet
28
+
29
+ match do |stack|
30
+ if !stack.is_a?(Hash)
31
+ raise ArgumentError, "You must pass a hash to this expectation"
32
+ end
33
+
34
+ change_set_result = generate_change_set(stack)
35
+
36
+ if change_set_result.status == 'FAILED'
37
+ @error = "Change set creation failed: #{change_set_result.status_reason}"
38
+ return false
39
+ end
40
+
41
+ if !change_set_result.changes.any? { |change| change.resource_type == resource_type }
42
+ @error = "Change set does not include resource type #{resource_type}"
43
+ return false
44
+ end
45
+
46
+ if !change_set_result.changes.any? { |change| change.logical_resource_id == resource_id }
47
+ @error = %Q(
48
+ Change set does not include a resource type #{resource_type} with the id #{resource_id}
49
+ Found the following resources with the same Resource Type:\n#{resource_ids_with_resource_type(change_set_result, resource_type).join("\n")}
50
+ )
51
+ return false
52
+ end
53
+ true
54
+ end
55
+
56
+ failure_message do
57
+ @error
58
+ end
59
+ end
60
+
61
+ RSpec::Matchers.define :have_change_set_failed do
62
+ include CloudFormationRSpec::Matchers::ChangeSet
63
+
64
+ match do |stack|
65
+ change_set_result = generate_change_set(stack)
66
+
67
+ if change_set_result.status != 'FAILED'
68
+ @error = "Change set creation succeeded"
69
+ return false
70
+ end
71
+
72
+ true
73
+ end
74
+
75
+ failure_message do
76
+ @error
77
+ end
78
+ end
@@ -0,0 +1,47 @@
1
+ require 'json'
2
+ require 'yaml'
3
+
4
+ RSpec::Matchers.define :have_output_including do |output_name|
5
+ def json_template?(template_body)
6
+ template_body =~ /^\s*{/ # ignore leading whitespaces
7
+ end
8
+
9
+ def compile_sparkle_template(stack)
10
+ stack[:compile_state] ||= {}
11
+
12
+ template = CloudFormationRSpec::Sparkle.compile_sparkle_template(stack[:template_file], stack[:compile_state])
13
+ JSON.load(template)
14
+ end
15
+
16
+ match do |input|
17
+ if input.is_a?(Hash) && input[:compiler] == :sparkleformation
18
+ unless input[:template_file]
19
+ raise ArgumentError, "You must pass a hash to this expectation with at least the :template_file option"
20
+ end
21
+
22
+ decode_function = lambda { |s| compile_sparkle_template(s) }
23
+ elsif !input.is_a?(String)
24
+ raise ArgumentError, "You must pass a hash for SparkleFormation templates, or a string for YAML/JSON templates"
25
+ elsif json_template?(input)
26
+ decode_function = lambda { |tmpl| JSON.load(tmpl) }
27
+ else
28
+ decode_function = lambda { |tmpl| YAML.safe_load(tmpl) }
29
+ end
30
+
31
+ begin
32
+ template = decode_function.call(input)
33
+ rescue JSON::ParserError, Psych::SyntaxError, CloudFormationRSpec::Sparkle::InvalidTemplate => error
34
+ raise SyntaxError, "Unable to parse template: #{error}"
35
+ end
36
+ outputs = template["Outputs"].nil? ? [] : template["Outputs"].keys
37
+
38
+ @actual = outputs
39
+ outputs.include?(output_name)
40
+ end
41
+
42
+ diffable
43
+
44
+ failure_message do
45
+ @error
46
+ end
47
+ end
@@ -0,0 +1,48 @@
1
+ require 'aws-sdk-cloudformation'
2
+
3
+ module CloudFormationRSpec::Matchers::Validate
4
+ def validate_cf_template(template_body)
5
+ client = Aws::CloudFormation::Client.new
6
+ begin
7
+ client.validate_template(template_body: template_body)
8
+ rescue Aws::CloudFormation::Errors::ValidationError => e
9
+ @error = e.message
10
+ return false
11
+ end
12
+ true
13
+ end
14
+ end
15
+
16
+ RSpec::Matchers.define :be_valid do
17
+ include CloudFormationRSpec::Matchers::Validate
18
+ match do |cf_template|
19
+ validate_cf_template(cf_template)
20
+ end
21
+
22
+ failure_message do
23
+ @error
24
+ end
25
+ end
26
+
27
+ RSpec::Matchers.define :be_valid_sparkleformation do
28
+ include CloudFormationRSpec::Matchers::Validate
29
+ match do |stack|
30
+ if !stack.is_a?(Hash) || !stack[:template_file]
31
+ raise ArgumentError, "You must pass a hash to this expectation with at least the :template_file option"
32
+ end
33
+
34
+ stack[:compile_state] ||= {}
35
+
36
+ begin
37
+ template_body = CloudFormationRSpec::Sparkle.compile_sparkle_template(stack[:template_file], stack[:compile_state])
38
+ rescue CloudFormationRSpec::Sparkle::InvalidTemplate => error
39
+ @error = error
40
+ return false
41
+ end
42
+ validate_cf_template(template_body)
43
+ end
44
+
45
+ failure_message do
46
+ @error
47
+ end
48
+ end
@@ -0,0 +1,12 @@
1
+ class CloudFormationRSpec::ResourceChange
2
+ attr_reader :resource_type, :logical_resource_id
3
+
4
+ def initialize(resource_type, logical_resource_id)
5
+ @resource_type = resource_type
6
+ @logical_resource_id = logical_resource_id
7
+ end
8
+
9
+ def ==(expected)
10
+ expected.is_a?(self.class) && self.resource_type == expected.resource_type && self.logical_resource_id == expected.logical_resource_id
11
+ end
12
+ end
@@ -0,0 +1,20 @@
1
+ class CloudFormationRSpec::Sparkle
2
+ InvalidTemplate = Class.new(StandardError)
3
+ InvalidSparkleTemplate = Class.new(InvalidTemplate)
4
+ InvalidCloudFormationTemplate = Class.new(InvalidTemplate)
5
+
6
+ def self.compile_sparkle_template(template_file, compile_state)
7
+ begin
8
+ sparkle_template = ::SparkleFormation.compile(template_file, :sparkle)
9
+ rescue RuntimeError, SyntaxError => error
10
+ raise InvalidSparkleTemplate.new("Error compiling template into SparkleTemplate #{error.message}")
11
+ end
12
+
13
+ begin
14
+ sparkle_template.compile_state = compile_state
15
+ sparkle_template.to_json
16
+ rescue => error
17
+ raise InvalidCloudFormationTemplate.new("Error compiling template into CloudFormation #{error.message}")
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,142 @@
1
+ require 'spec_helper'
2
+
3
+ describe CloudFormationRSpec::ChangeSet do
4
+ let(:template_body) { '{"Description": "Foo"}' }
5
+ let(:parameters) { {"VpcCidr" => "10.0.0.0/16"} }
6
+ let(:cf_stub) { instance_double(Aws::CloudFormation::Client) }
7
+ let(:change_set_mock) { instance_double(Aws::CloudFormation::Types::DescribeChangeSetOutput) }
8
+ let(:change_set_create_mock) { instance_double(Aws::CloudFormation::Types::CreateChangeSetOutput) }
9
+ let(:stacks_mock) { instance_double(Aws::CloudFormation::Types::DescribeStacksOutput) }
10
+ let(:stack_mock) { instance_double(Aws::CloudFormation::Types::Stack) }
11
+ let(:aws_change_mock) { instance_double(Aws::CloudFormation::Types::Change) }
12
+ let(:aws_resource_change_mock) { instance_double(Aws::CloudFormation::Types::ResourceChange) }
13
+ let(:uuid) { "a7ad0965-7395-4660-b607-47b13b1d16c2" }
14
+
15
+ before do
16
+ allow(Aws::CloudFormation::Client).to receive(:new).and_return(cf_stub)
17
+ allow(cf_stub).to receive(:create_change_set).and_return(change_set_create_mock)
18
+ allow(change_set_create_mock).to receive(:id).and_return("123")
19
+ allow(cf_stub).to receive(:delete_stack)
20
+ allow(cf_stub).to receive(:delete_change_set)
21
+ allow(cf_stub).to receive(:describe_change_set).and_return(change_set_mock)
22
+ allow(cf_stub).to receive(:describe_stacks).and_return(stacks_mock)
23
+ allow(cf_stub).to receive(:wait_until).and_return(true)
24
+ allow(change_set_mock).to receive(:status).and_return("CREATE_COMPLETE")
25
+ allow(change_set_mock).to receive(:changes).and_return([aws_change_mock])
26
+ allow(aws_change_mock).to receive(:resource_change).and_return(aws_resource_change_mock)
27
+ allow(stacks_mock).to receive(:stacks).and_return([stack_mock])
28
+ allow(stack_mock).to receive(:stack_status).and_return("REVIEW_IN_PROGRESS")
29
+ allow(aws_resource_change_mock).to receive(:resource_type).and_return("AWS::EC2::VPC")
30
+ allow(aws_resource_change_mock).to receive(:logical_resource_id).and_return("Foo")
31
+ allow(SecureRandom).to receive(:uuid).and_return(uuid)
32
+ end
33
+
34
+ after do
35
+ CloudFormationRSpec::ChangeSet.flush_cache
36
+ end
37
+
38
+ context 'a sparkleformation template' do
39
+ let(:template_file) { File.join("spec", "fixtures", "vpc.rb") }
40
+ let(:parameters) { {} }
41
+ let(:compile_state) { {} }
42
+ subject { described_class.from_sparkleformation_template(template_file: template_file, compile_state: compile_state, parameters: parameters) }
43
+
44
+ context 'that does compile' do
45
+ let(:template_file) { File.join('spec', 'fixtures', 'valid_sparkle_vpc_template.rb') }
46
+
47
+ context 'with parameters' do
48
+ let(:parameters) { {"VpcCidr" => "10.0.0.0/16"} }
49
+ it 'succeeds' do
50
+ expect(subject.status).to eq("CREATE_COMPLETE")
51
+ end
52
+ end
53
+
54
+ context 'with compile state' do
55
+ let(:compile_state) { {public_subnets: ["10.0.0.0/24", "10.0.1.0/24"], private_subnets: ["10.0.2.0/24", "10.0.3.0/24"]} }
56
+ it 'succeeds' do
57
+ expect(subject.status).to eq("CREATE_COMPLETE")
58
+ end
59
+ end
60
+
61
+ context 'with no parameters or compile state' do
62
+ it 'succeeds' do
63
+ expect(subject.status).to eq("CREATE_COMPLETE")
64
+ end
65
+ end
66
+ end
67
+ end
68
+
69
+ context 'a valid change set' do
70
+ subject(:subject_default) { described_class.new(template_body: template_body, parameters: parameters) }
71
+ subject(:cached_subject_default) { described_class.new(template_body: template_body, parameters: parameters) }
72
+ subject(:different_parameters) { described_class.new(template_body: template_body, parameters: {}) }
73
+ subject(:different_template) { described_class.new(template_body: "{}", parameters: parameters) }
74
+
75
+ it 'calls create_change_set with the required parameters' do
76
+ expect(cf_stub).to receive(:create_change_set).with(
77
+ change_set_name: "CloudFormationRSpec-#{uuid}",
78
+ stack_name: "CloudFormationRSpec-#{uuid}",
79
+ change_set_type: 'CREATE',
80
+ template_body: template_body,
81
+ parameters: [
82
+ {
83
+ parameter_key: "VpcCidr",
84
+ parameter_value: "10.0.0.0/16"
85
+ }
86
+ ],
87
+ capabilities: ['CAPABILITY_IAM', 'CAPABILITY_NAMED_IAM'],
88
+ )
89
+ subject_default.create_change_set
90
+ end
91
+
92
+ it 'caches the change set between runs' do
93
+ expect(cf_stub).to receive(:create_change_set).once
94
+ subject_default.create_change_set
95
+ cached_subject_default.create_change_set
96
+ end
97
+
98
+ it 'restores the status from the cached run' do
99
+ subject_default.create_change_set
100
+ cached_subject_default.create_change_set
101
+ expect(cached_subject_default.status).to eq("CREATE_COMPLETE")
102
+ end
103
+
104
+ it 'restores the changes from the cached run' do
105
+ subject_default.create_change_set
106
+ cached_subject_default.create_change_set
107
+ expect(cached_subject_default.changes).to match_array([CloudFormationRSpec::ResourceChange.new("AWS::EC2::VPC", "Foo")])
108
+ end
109
+
110
+ it 'does not cache change sets when parameters are different' do
111
+ expect(cf_stub).to receive(:create_change_set).twice
112
+ subject_default.create_change_set
113
+ different_parameters.create_change_set
114
+ end
115
+
116
+ it 'does not cache change sets when the template is different' do
117
+ expect(cf_stub).to receive(:create_change_set).twice
118
+ subject_default.create_change_set
119
+ different_template.create_change_set
120
+ end
121
+
122
+ it 'deletes the stack and change set' do
123
+ expect(cf_stub).to receive(:delete_stack)
124
+ expect(cf_stub).to receive(:delete_change_set)
125
+ subject_default.create_change_set
126
+ end
127
+ end
128
+
129
+ context 'an invalid change' do
130
+ subject { described_class.new(template_body: template_body, parameters: parameters) }
131
+ before do
132
+ allow(cf_stub).to receive(:wait_until).and_raise(Aws::Waiters::Errors::WaiterFailed)
133
+ allow(change_set_mock).to receive(:status).and_return("FAILED")
134
+ end
135
+
136
+ it 'just deletes the change set' do
137
+ expect(cf_stub).not_to receive(:delete_stack)
138
+ expect(cf_stub).to receive(:delete_change_set)
139
+ subject.create_change_set
140
+ end
141
+ end
142
+ end
@@ -0,0 +1,8 @@
1
+ SparkleFormation.new(:vpc) do
2
+ resources.vpc do
3
+ type "AWS::EC2::VPC
4
+ properties do
5
+ cidr "10.0.0.0/16"
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,16 @@
1
+ SparkleFormation.new(:vpc,
2
+ {
3
+ compile_time_parameters: {
4
+ vpc_cidr: {
5
+ type: :string,
6
+ }
7
+ }
8
+ }
9
+ ) do
10
+ parameters.vpc_cidr do
11
+ description 'VPC CIDR'
12
+ type 'String'
13
+ constraint_description 'CIDR block parameter must be in the form x.x.x.x/16-28'
14
+ default state!(:vpc_cidr)
15
+ end
16
+ end
@@ -0,0 +1,20 @@
1
+ SparkleFormation.new(:vpc) do
2
+ resources.vpc do
3
+ type "AWS::EC2::VPC"
4
+ properties do
5
+ cidr "10.0.0.0/16"
6
+ end
7
+ end
8
+
9
+ outputs do
10
+ vpc_id do
11
+ value ref!(:vpc)
12
+ description "The VPC ID"
13
+ end
14
+
15
+ vpc_cidr do
16
+ value "10.0.0.0/16"
17
+ description "The VPC CIDR"
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,20 @@
1
+ {
2
+ "AWSTemplateFormatVersion" : "2010-09-09",
3
+ "Resources" : {
4
+ "myVPC" : {
5
+ "Type" : "AWS::EC2::VPC",
6
+ "Properties" : {
7
+ "CidrBlock" : "10.0.0.0/16",
8
+ "EnableDnsSupport" : "false",
9
+ "EnableDnsHostnames" : "false",
10
+ "InstanceTenancy" : "dedicated",
11
+ "Tags" : [ {"Key" : "foo", "Value" : "bar"} ]
12
+ }
13
+ }
14
+ },
15
+ "Outputs" : {
16
+ "VpcId": {
17
+ "Value": {"Ref": "myVPC"}
18
+ }
19
+ }
20
+ }
@@ -0,0 +1,15 @@
1
+ AWSTemplateFormatVersion: '2010-09-09'
2
+ Resources:
3
+ myVPC:
4
+ Type: AWS::EC2::VPC
5
+ Properties:
6
+ CidrBlock: 10.0.0.0/16
7
+ EnableDnsSupport: 'false'
8
+ EnableDnsHostnames: 'false'
9
+ InstanceTenancy: dedicated
10
+ Tags:
11
+ - Key: foo
12
+ Value: bar
13
+ Outputs:
14
+ VpcId:
15
+ Value: !Ref myVPC
@@ -0,0 +1,136 @@
1
+ require 'spec_helper'
2
+
3
+ describe 'contain_in_change_set' do
4
+ let(:change_set_mock) { instance_double(CloudFormationRSpec::ChangeSet) }
5
+ let(:stack) {{
6
+ template_body: '{"Parameters": {"VpcCidr": {"Type": "String"}},"Resources": {"Foo": {"Type" : "AWS::EC2::VPC","Properties": {"Cidr" : {"Ref": "VpcCidr"}}}}}',
7
+ parameters: {
8
+ "VpcCidr" => "10.0.0.0/16"
9
+ }
10
+ }}
11
+ let(:sparkle_stack) {{
12
+ compiler: :sparkleformation,
13
+ template_file: File.join("templates", "vpc.rb"),
14
+ compile_state: {public_subnets: ["10.0.0.0/24", "10.0.1.0/24"], private_subnets: ["10.0.2.0/24", "10.0.3.0/24"]},
15
+ parameters: {
16
+ "VpcCidr" => "10.0.0.0/16",
17
+ }
18
+ }}
19
+ let(:uuid) { "d7ad0965-7395-4660-b607-47b13b1d16c2" }
20
+ let(:web_server_change_mock) { instance_double(CloudFormationRSpec::ResourceChange) }
21
+ let(:vpc_change_mock) { instance_double(CloudFormationRSpec::ResourceChange) }
22
+ before do
23
+ allow(CloudFormationRSpec::ChangeSet).to receive(:new).and_return(change_set_mock)
24
+ allow(CloudFormationRSpec::ChangeSet).to receive(:from_sparkleformation_template).and_return(change_set_mock)
25
+ allow(CloudFormationRSpec::ChangeSet).to receive(:from_cloudformation_template).and_return(change_set_mock)
26
+ allow(SecureRandom).to receive(:uuid).and_return(uuid)
27
+ end
28
+
29
+ after do
30
+ CloudFormationRSpec::ChangeSet.flush_cache
31
+ end
32
+
33
+ context 'a valid cloudformation template' do
34
+ before do
35
+ allow(change_set_mock).to receive(:status).and_return("CREATE_COMPLETE")
36
+ allow(change_set_mock).to receive(:changes).and_return([vpc_change_mock, web_server_change_mock])
37
+ allow(web_server_change_mock).to receive(:resource_type).and_return("AWS::EC2::Instance")
38
+ allow(web_server_change_mock).to receive(:logical_resource_id).and_return("WebServer")
39
+ allow(vpc_change_mock).to receive(:resource_type).and_return("AWS::EC2::VPC")
40
+ allow(vpc_change_mock).to receive(:logical_resource_id).and_return("vpc")
41
+ end
42
+
43
+ it 'succeeds when there is a matching resource' do
44
+ expect(stack).to contain_in_change_set("AWS::EC2::VPC", "vpc")
45
+ end
46
+
47
+ it 'fails when there is no matching resource id' do
48
+ expect(stack).not_to contain_in_change_set("AWS::EC2::VPC", "foo")
49
+ end
50
+
51
+ it 'fails when there is no matching resource type' do
52
+ expect(stack).not_to contain_in_change_set("AWS::EC2::Foo", "vpc")
53
+ end
54
+ end
55
+
56
+ context 'an invalid sparkleformation template' do
57
+ let(:template_file) { File.join('spec', 'fixtures', 'invalid_sparkle_vpc_template.rb') }
58
+ let(:stack) {{
59
+ compiler: :sparkleformation,
60
+ template_file: template_file,
61
+ compile_state: {}
62
+ }}
63
+ before do
64
+ allow(CloudFormationRSpec::ChangeSet).to receive(:from_sparkleformation_template).and_raise(CloudFormationRSpec::Sparkle::InvalidSparkleTemplate)
65
+ allow(change_set_mock).to receive(:status).and_return("FAILED")
66
+ end
67
+
68
+ it 'fails' do
69
+ expect { expect(stack).not_to contain_in_change_set("AWS::EC2::Foo", "vpc") }.to raise_error(CloudFormationRSpec::Sparkle::InvalidSparkleTemplate)
70
+ end
71
+ end
72
+ end
73
+
74
+ describe 'have_change_set_failed' do
75
+ let(:change_set_mock) { instance_double(CloudFormationRSpec::ChangeSet) }
76
+ let(:stack) {{
77
+ template_body: '{"Parameters": {"VpcCidr": {"Type": "String"}},"Resources": {"Foo": {"Type" : "AWS::EC2::VPC","Properties": {"Cidr" : {"Ref": "VpcCidr"}}}}}',
78
+ parameters: {
79
+ "VpcCidr" => "10.0.0.0/16"
80
+ }
81
+ }}
82
+ let(:sparkle_stack) {{
83
+ compiler: :sparkleformation,
84
+ template_file: File.join("templates", "vpc.rb"),
85
+ compile_state: {public_subnets: ["10.0.0.0/24", "10.0.1.0/24"], private_subnets: ["10.0.2.0/24", "10.0.3.0/24"]},
86
+ parameters: {
87
+ "VpcCidr" => "10.0.0.0/16",
88
+ }
89
+ }}
90
+ before do
91
+ allow(CloudFormationRSpec::ChangeSet).to receive(:new).and_return(change_set_mock)
92
+ allow(CloudFormationRSpec::ChangeSet).to receive(:from_sparkleformation_template).and_return(change_set_mock)
93
+ allow(CloudFormationRSpec::ChangeSet).to receive(:from_cloudformation_template).and_return(change_set_mock)
94
+ end
95
+
96
+ after do
97
+ CloudFormationRSpec::ChangeSet.flush_cache
98
+ end
99
+
100
+ context 'a valid cloudformation template' do
101
+ before do
102
+ allow(change_set_mock).to receive(:status).and_return("CREATE_COMPLETE")
103
+ end
104
+
105
+ it 'fails' do
106
+ expect(stack).not_to have_change_set_failed
107
+ end
108
+ end
109
+
110
+ context 'an invalid change' do
111
+ before do
112
+ allow(change_set_mock).to receive(:status).and_return("FAILED")
113
+ end
114
+
115
+ it 'succeeds' do
116
+ expect(stack).to have_change_set_failed
117
+ end
118
+ end
119
+
120
+ context 'an invalid sparkleformation template' do
121
+ let(:template_file) { File.join('spec', 'fixtures', 'invalid_sparkle_vpc_template.rb') }
122
+ let(:stack) {{
123
+ compiler: :sparkleformation,
124
+ template_file: template_file,
125
+ compile_state: {}
126
+ }}
127
+ before do
128
+ allow(CloudFormationRSpec::ChangeSet).to receive(:from_sparkleformation_template).and_raise(CloudFormationRSpec::Sparkle::InvalidSparkleTemplate)
129
+ allow(change_set_mock).to receive(:status).and_return("FAILED")
130
+ end
131
+
132
+ it 'fails' do
133
+ expect { expect(stack).to have_change_set_failed }.to raise_error(CloudFormationRSpec::Sparkle::InvalidSparkleTemplate)
134
+ end
135
+ end
136
+ end
@@ -0,0 +1,59 @@
1
+ require 'spec_helper'
2
+
3
+ describe 'have_output_including' do
4
+ context 'sparkleformation template' do
5
+ let(:template_file) { File.join('spec', 'fixtures', 'valid_sparkle_vpc_template.rb') }
6
+ let(:stack) {{
7
+ compiler: :sparkleformation,
8
+ template_file: template_file,
9
+ compile_state: {}
10
+ }}
11
+
12
+ it 'has a vpc_id output' do
13
+ expect(stack).to have_output_including("VpcId")
14
+ end
15
+
16
+ it 'has vpc_cidr output' do
17
+ expect(stack).to have_output_including("VpcCidr")
18
+ end
19
+
20
+ it 'does not have subnet_id output' do
21
+ expect(stack).not_to have_output_including("SubnetId")
22
+ end
23
+ end
24
+
25
+ context 'yaml template' do
26
+ let(:template_body) { File.read(File.join('spec', 'fixtures', 'valid_vpc_template.yml')) }
27
+
28
+ it 'has a vpc_id output' do
29
+ expect(template_body).to have_output_including("VpcId")
30
+ end
31
+
32
+ it 'does not have vpc_cidr output' do
33
+ expect(template_body).not_to have_output_including("VpcCidr")
34
+ end
35
+ end
36
+
37
+ context 'json template' do
38
+ let(:template_body) { File.read(File.join('spec', 'fixtures', 'valid_vpc_template.json')) }
39
+
40
+ it 'has a vpc_id output' do
41
+ expect(template_body).to have_output_including("VpcId")
42
+ end
43
+
44
+ it 'does not have vpc_cidr output' do
45
+ expect(template_body).not_to have_output_including("VpcCidr")
46
+ end
47
+
48
+ it 'provides a diff of the outputs' do
49
+ expect { expect(template_body).not_to have_output_including("VpcId") }.to raise_error(RSpec::Expectations::ExpectationNotMetError, 'expected ["VpcId"] not to have output including "VpcId"')
50
+ end
51
+ end
52
+
53
+ context 'garbage template' do
54
+ let(:template_body) { ' {lkajdflkasdjf' }
55
+ it 'raises an ArgumentError' do
56
+ expect{ expect(template_body).to have_output_including("VpcId") }.to raise_error(SyntaxError)
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,88 @@
1
+ require 'spec_helper'
2
+
3
+ describe 'be_valid' do
4
+ let(:cf_stub) { instance_double(Aws::CloudFormation::Client) }
5
+ before do
6
+ allow(Aws::CloudFormation::Client).to receive(:new).and_return(cf_stub)
7
+ end
8
+
9
+ context 'the stack is valid' do
10
+ before do
11
+ allow(cf_stub).to receive(:validate_template)
12
+ end
13
+
14
+ it 'succeeds' do
15
+ expect('{"Description": "My Template", "Resources": {"Foo": {"Type" : "AWS::EC2::Instance","Properties" : {"ImageId" : "ami-2f726546"}}}}').to be_valid
16
+ end
17
+ end
18
+
19
+ context 'the stack is not valid' do
20
+ let(:template) { '{"Description": "My Template"}' }
21
+ before do
22
+ allow(cf_stub).to receive(:validate_template).with(template_body: template).and_raise(Aws::CloudFormation::Errors::ValidationError.new(Seahorse::Client::RequestContext.new(), "Template format error: At least one Resources member must be defined."))
23
+ end
24
+
25
+ it 'fails' do
26
+ expect(template).not_to be_valid
27
+ end
28
+ end
29
+ end
30
+
31
+ describe 'be_valid_sparkleformation' do
32
+ let(:cf_stub) { instance_double(Aws::CloudFormation::Client) }
33
+ let(:compile_state) { {} }
34
+ let(:stack) {{
35
+ compiler: :sparkleformation,
36
+ template_file: template_file,
37
+ compile_state: compile_state
38
+ }}
39
+ before do
40
+ allow(Aws::CloudFormation::Client).to receive(:new).and_return(cf_stub)
41
+ end
42
+
43
+ context 'the stack is valid' do
44
+ let(:template_file) { File.join('spec', 'fixtures', 'valid_sparkle_vpc_template.rb') }
45
+ before do
46
+ allow(cf_stub).to receive(:validate_template)
47
+ end
48
+
49
+ it 'succeeds' do
50
+ expect(stack).to be_valid_sparkleformation
51
+ end
52
+ end
53
+
54
+ context 'the stack is not valid cloudformation' do
55
+ let(:template_file) { File.join('spec', 'fixtures', 'valid_sparkle_vpc_template.rb') }
56
+ before do
57
+ allow(cf_stub).to receive(:validate_template).and_raise(Aws::CloudFormation::Errors::ValidationError.new(Seahorse::Client::RequestContext.new(), "Template format error: At least one Resources member must be defined."))
58
+ end
59
+
60
+ it 'fails' do
61
+ expect(stack).not_to be_valid_sparkleformation
62
+ end
63
+ end
64
+
65
+ context 'the stack is not valid sparkleformation' do
66
+ let(:template_file) { File.join('spec', 'fixtures', 'invalid_sparkle_vpc_template.rb') }
67
+ before do
68
+ allow(cf_stub).to receive(:validate_template)
69
+ end
70
+
71
+ it 'fails' do
72
+ expect(cf_stub).not_to receive(:validate_template)
73
+ expect(stack).not_to be_valid_sparkleformation
74
+ end
75
+ end
76
+
77
+ context 'the stack has compile time parameters' do
78
+ let(:template_file) { File.join('spec', 'fixtures', 'template_with_compile_parameters.rb') }
79
+ let(:compile_state) { {vpc_cidr: "10.0.0.0/16"} }
80
+ before do
81
+ allow(cf_stub).to receive(:validate_template)
82
+ end
83
+
84
+ it 'succeed' do
85
+ expect(stack).to be_valid_sparkleformation
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,30 @@
1
+ require 'spec_helper'
2
+
3
+ describe CloudFormationRSpec::Sparkle do
4
+ let(:template_file) { File.join("spec", "fixtures", "vpc.rb") }
5
+ let(:compile_state) { {} }
6
+ subject { described_class.compile_sparkle_template(template_file, compile_state) }
7
+
8
+ context 'that doesnt compile to sparkleformation' do
9
+ before do
10
+ allow(SparkleFormation).to receive(:compile).and_raise(RuntimeError)
11
+ end
12
+
13
+ it 'raises InvalidSparkleTemplate' do
14
+ expect { subject }.to raise_error(described_class::InvalidSparkleTemplate)
15
+ end
16
+ end
17
+
18
+ context 'that doesnt compile to cloudformation' do
19
+ let(:sparkle_stub) { instance_double(SparkleFormation) }
20
+ before do
21
+ allow(SparkleFormation).to receive(:compile).and_return(sparkle_stub)
22
+ allow(sparkle_stub).to receive(:compile_state=)
23
+ allow(sparkle_stub).to receive(:to_json).and_raise(RuntimeError)
24
+ end
25
+
26
+ it 'raises InvalidCloudFormationTemplate' do
27
+ expect { subject }.to raise_error(described_class::InvalidCloudFormationTemplate)
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,2 @@
1
+ require 'rspec'
2
+ require 'cloudformation_rspec'
metadata ADDED
@@ -0,0 +1,133 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: cloudformation_rspec
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.9
5
+ platform: ruby
6
+ authors:
7
+ - Patrick Robinson
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2018-08-14 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rspec
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '3'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '3'
27
+ - !ruby/object:Gem::Dependency
28
+ name: sparkle_formation
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '3'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '3'
41
+ - !ruby/object:Gem::Dependency
42
+ name: aws-sdk-cloudformation
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '1'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '1'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rake
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ description: CloudFormation RSpec matchers
70
+ email:
71
+ - patrick.robinson@envato.com
72
+ executables: []
73
+ extensions: []
74
+ extra_rdoc_files: []
75
+ files:
76
+ - Gemfile
77
+ - README.md
78
+ - Rakefile
79
+ - cloudformation-rspec.gemspec
80
+ - lib/cloudformation_rspec.rb
81
+ - lib/cloudformation_rspec/change_set.rb
82
+ - lib/cloudformation_rspec/matchers.rb
83
+ - lib/cloudformation_rspec/matchers/change_set.rb
84
+ - lib/cloudformation_rspec/matchers/output.rb
85
+ - lib/cloudformation_rspec/matchers/validate.rb
86
+ - lib/cloudformation_rspec/resource_change.rb
87
+ - lib/cloudformation_rspec/sparkle.rb
88
+ - spec/change_set_spec.rb
89
+ - spec/fixtures/invalid_sparkle_vpc_template.rb
90
+ - spec/fixtures/template_with_compile_parameters.rb
91
+ - spec/fixtures/valid_sparkle_vpc_template.rb
92
+ - spec/fixtures/valid_vpc_template.json
93
+ - spec/fixtures/valid_vpc_template.yml
94
+ - spec/matchers/change_set_spec.rb
95
+ - spec/matchers/output_spec.rb
96
+ - spec/matchers/validate_spec.rb
97
+ - spec/sparkle_spec.rb
98
+ - spec/spec_helper.rb
99
+ homepage: https://github.com/envato/cloudformation_rspec
100
+ licenses: []
101
+ metadata: {}
102
+ post_install_message:
103
+ rdoc_options: []
104
+ require_paths:
105
+ - lib
106
+ required_ruby_version: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ required_rubygems_version: !ruby/object:Gem::Requirement
112
+ requirements:
113
+ - - ">="
114
+ - !ruby/object:Gem::Version
115
+ version: '0'
116
+ requirements: []
117
+ rubyforge_project:
118
+ rubygems_version: 2.7.6
119
+ signing_key:
120
+ specification_version: 4
121
+ summary: Test your CloudFormation templates
122
+ test_files:
123
+ - spec/change_set_spec.rb
124
+ - spec/fixtures/invalid_sparkle_vpc_template.rb
125
+ - spec/fixtures/template_with_compile_parameters.rb
126
+ - spec/fixtures/valid_sparkle_vpc_template.rb
127
+ - spec/fixtures/valid_vpc_template.json
128
+ - spec/fixtures/valid_vpc_template.yml
129
+ - spec/matchers/change_set_spec.rb
130
+ - spec/matchers/output_spec.rb
131
+ - spec/matchers/validate_spec.rb
132
+ - spec/sparkle_spec.rb
133
+ - spec/spec_helper.rb