├── .travis.yml ├── Gemfile ├── .rspec ├── .rubocop.yml ├── spec ├── spec_helper.rb ├── terraform_spec.rb ├── create_a_plan_matcher_spec.rb └── require_variables_matcher_spec.rb ├── lib ├── rspec-terraform │ ├── version.rb │ └── matchers │ │ ├── create_a_plan.rb │ │ └── require_variables.rb └── rspec-terraform.rb ├── Rakefile ├── bin ├── setup └── console ├── .gitignore ├── TODO.md ├── rspec-terraform.gemspec ├── LICENSE.txt └── README.md /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | rvm: 3 | - 2.1.4 4 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gemspec 4 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --require spec_helper 2 | --format documentation 3 | --color 4 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | Metrics/LineLength: 2 | Max: 120 3 | 4 | Documentation: 5 | Enabled: false 6 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | $LOAD_PATH.unshift File.expand_path('../../lib', __FILE__) 2 | require 'rspec-terraform' 3 | -------------------------------------------------------------------------------- /lib/rspec-terraform/version.rb: -------------------------------------------------------------------------------- 1 | module RSpec 2 | module Terraform 3 | VERSION = '0.0.1' 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'bundler/gem_tasks' 2 | require 'rspec/core/rake_task' 3 | 4 | RSpec::Core::RakeTask.new(:spec) 5 | 6 | task default: :spec 7 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | 5 | bundle install 6 | 7 | # Do any other automated setup that you need to do here 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /Gemfile.lock 4 | /_yardoc/ 5 | /coverage/ 6 | /doc/ 7 | /pkg/ 8 | /spec/reports/ 9 | /tmp/ 10 | .idea 11 | -------------------------------------------------------------------------------- /spec/terraform_spec.rb: -------------------------------------------------------------------------------- 1 | describe RSpec::Terraform do 2 | it 'has a version number' do 3 | expect(RSpec::Terraform::VERSION).not_to be nil 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | # TODO 2 | 3 | 1. package latest terraform binary inside gem 4 | 2. package/fetch older terraform binaries? 5 | 3. add more providers (AWS only right now) 6 | 4. matcher for default values for variables 7 | 5. matcher for output variables (we have to apply the plan for this to work...) 8 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'bundler/setup' 4 | require 'rspec-terraform' 5 | 6 | # You can add fixtures and/or initialization code here to make experimenting 7 | # with your gem easier. You can also use a different console, if you like. 8 | 9 | # (If you use this, don't forget to add pry to your Gemfile!) 10 | # require "pry" 11 | # Pry.start 12 | 13 | require 'irb' 14 | IRB.start 15 | -------------------------------------------------------------------------------- /lib/rspec-terraform/matchers/create_a_plan.rb: -------------------------------------------------------------------------------- 1 | RSpec::Matchers.define :create_a_plan do 2 | match do |command| 3 | Open3.popen3(command) do |_stdin, stdout, stderr, _wait_thr| 4 | @stderr = stderr.read 5 | @stdout = stdout.read 6 | end 7 | 8 | plan_file = File.exist?('plan.tf') 9 | tfstate_file = File.exist?('terraform.tfstate') 10 | 11 | plan_file && tfstate_file && @stderr.empty? 12 | end 13 | 14 | failure_message do 15 | "Expected plan.tf and terraform.tfstate files to be created.\n"\ 16 | "Terraform output: \n#{@stderr}" 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /lib/rspec-terraform/matchers/require_variables.rb: -------------------------------------------------------------------------------- 1 | RSpec::Matchers.define :require_variables do |expected_variables| 2 | match do |command| 3 | Open3.popen3(command) do |_stdin, _stdout, stderr, _wait_thr| 4 | @stderr = stderr.read 5 | end 6 | 7 | @missing_variables = @stderr.scan(/not set: (\w+)/).flatten 8 | @missing_variables.sort == expected_variables.sort 9 | end 10 | 11 | failure_message do 12 | message = "The test expects variables: #{expected_variables}\n" 13 | message += "Terraform expects variables: #{@missing_variables}\n" 14 | 15 | missing = @missing_variables - expected_variables 16 | message += "The missing variables were: #{missing}" unless missing.empty? 17 | 18 | extra = expected_variables - @missing_variables 19 | message += "The extra variables were: #{extra}" unless extra.empty? 20 | 21 | message 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/rspec-terraform.rb: -------------------------------------------------------------------------------- 1 | require 'bundler/setup' 2 | require 'rspec/expectations' 3 | require 'open3' 4 | require 'rspec' 5 | 6 | require_relative 'rspec-terraform/version' 7 | require_relative 'rspec-terraform/matchers/create_a_plan' 8 | require_relative 'rspec-terraform/matchers/require_variables' 9 | 10 | RSpec.configure do |config| 11 | # https://github.com/hashicorp/terraform/pull/2730 12 | # Correct credentials must be provided 13 | config.before(:all, provider: :aws) do 14 | unless ENV['TF_VAR_access_key'] && ENV['TF_VAR_secret_key'] && ENV['TF_VAR_region'] 15 | fail 'You must provide your AWS credentials and region as environment variables: '\ 16 | "'TF_VAR_access_key', 'TF_VAR_secret_key', 'TF_VAR_region'" 17 | end 18 | end 19 | 20 | config.after(:each, provider: :aws) do 21 | cleanup! 22 | end 23 | 24 | def cleanup! 25 | `rm -f plan.tf` 26 | `rm -f terraform.tfstate` 27 | `rm -f provider.tf` 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /rspec-terraform.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | lib = File.expand_path('../lib', __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require 'rspec-terraform/version' 5 | 6 | Gem::Specification.new do |spec| 7 | spec.name = 'rspec-terraform' 8 | spec.version = RSpec::Terraform::VERSION 9 | spec.authors = ['Ben Snape'] 10 | spec.email = ['bsnape@gmail.com'] 11 | 12 | spec.summary = 'RSpec test fixtures for Terraform modules.' 13 | spec.homepage = 'https://github.com/bsnape/rspec-terraform' 14 | spec.license = 'MIT' 15 | 16 | spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) } 17 | spec.bindir = 'exe' 18 | spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } 19 | spec.require_paths = ['lib'] 20 | 21 | spec.add_runtime_dependency 'rspec', '~> 3.3.0' 22 | 23 | spec.add_development_dependency 'bundler', '~> 1.9' 24 | spec.add_development_dependency 'rake', '~> 10.4.2' 25 | spec.add_development_dependency 'rubocop', '~> 0.33.0' 26 | end 27 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Ben Snape 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /spec/create_a_plan_matcher_spec.rb: -------------------------------------------------------------------------------- 1 | describe 'create a plan matcher' do 2 | 3 | it 'expects plan.tf and terraform.tfstate to exist in the root directory' do 4 | create_plan_file 5 | create_state_file 6 | expect { expect('').to create_a_plan }.not_to output.to_stderr_from_any_process 7 | delete_plan_file 8 | delete_state_file 9 | end 10 | 11 | context 'when plan.tf does not exist in the root directory' do 12 | before do 13 | create_state_file 14 | end 15 | 16 | it 'raises an RSpec ExpectationNotMetError error' do 17 | expect { expect('').to create_a_plan }.to raise_error(RSpec::Expectations::ExpectationNotMetError) 18 | end 19 | 20 | after do 21 | delete_state_file 22 | end 23 | end 24 | 25 | context 'when terraform.tfstate does not exist in the root directory' do 26 | before do 27 | create_plan_file 28 | end 29 | 30 | it 'raises an RSpec ExpectationNotMetError error' do 31 | expect { expect('').to create_a_plan }.to raise_error(RSpec::Expectations::ExpectationNotMetError) 32 | end 33 | 34 | after do 35 | delete_plan_file 36 | end 37 | end 38 | 39 | it 'returns the terraform output when it errors' do 40 | error = RSpec::Expectations::ExpectationNotMetError 41 | error_message = /Error loading config: open foo: no such file or directory/ 42 | expect { expect('terraform plan foo').to create_a_plan }.to raise_error(error).with_message(error_message) 43 | end 44 | end 45 | 46 | def create_plan_file 47 | File.new('plan.tf', 'w+') 48 | end 49 | 50 | def create_state_file 51 | File.new('terraform.tfstate', 'w+') 52 | end 53 | 54 | def delete_plan_file 55 | File.delete 'plan.tf' 56 | end 57 | 58 | def delete_state_file 59 | File.delete 'terraform.tfstate' 60 | end 61 | -------------------------------------------------------------------------------- /spec/require_variables_matcher_spec.rb: -------------------------------------------------------------------------------- 1 | describe 'require variables matcher' do 2 | after(:each) do 3 | delete_variables_file 4 | end 5 | 6 | context 'when a module expects one variable' do 7 | before(:each) do 8 | variables = 'variable "foo" {}' 9 | create_variables_file variables 10 | end 11 | 12 | it 'passes the assertion when the correct variables are provided' do 13 | missing_variable = ['foo'] 14 | expect { expect('terraform plan -out=plan.tf').to require_variables(missing_variable) }.not_to raise_error 15 | end 16 | 17 | it 'identifies missing variables' do 18 | expected_variables = [] 19 | error = RSpec::Expectations::ExpectationNotMetError 20 | error_message = /The missing variables were: \["foo"\]/ 21 | expect { expect('terraform plan -out=plan.tf').to require_variables(expected_variables) }.to raise_error(error, error_message) 22 | end 23 | 24 | it 'identifies extra variables' do 25 | expected_variables = %w(foo bar) 26 | error = RSpec::Expectations::ExpectationNotMetError 27 | error_message = /The extra variables were: \["bar"\]/ 28 | expect { expect('terraform plan -out=plan.tf').to require_variables(expected_variables) }.to raise_error(error, error_message) 29 | end 30 | end 31 | 32 | context 'when a module expects multiple variables' do 33 | before(:each) do 34 | variables = "variable \"foo\" {}\nvariable \"bar\" {}" 35 | create_variables_file variables 36 | end 37 | 38 | it 'identifies multiple missing terraform variables in any order' do 39 | missing_variables = %w(bar foo) 40 | expect { expect('terraform plan -out=plan.tf').to require_variables(missing_variables) }.not_to raise_error 41 | end 42 | end 43 | end 44 | 45 | def create_variables_file(content) 46 | File.open('variables.tf', 'w+') { |f| f.write(content) } 47 | end 48 | 49 | def delete_variables_file 50 | File.delete 'variables.tf' 51 | end 52 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Rspec-Terraform 2 | 3 | * [Motivation](#motivation) 4 | * [Looking Ahead](#looking-ahead) 5 | * [Usage](#usage) 6 | * [Setup](#setup) 7 | * [Matchers](#matchers) 8 | * [Contributing](#contributing) 9 | 10 | ## Motivation 11 | 12 | [Terraform](https://github.com/hashicorp/terraform) is an awesome way of provisioning your infrastructure. However, like 13 | with any new tool, the ecosystem surrounding it is very immature. This can cause problems when sharing code or coming up 14 | against the rough edges of a tool in active development. 15 | 16 | The creation of `rspec-terraform` was initially intended to smooth the creation and sharing of common Terraform 17 | modules. Some sort of basic testing would ensure a stable and clearly defined interface for each module. 18 | 19 | ### Looking Ahead 20 | 21 | Eventually, a two-tiered approach to testing would be ideal. 22 | 23 | Provisioning non-trivial infrastructure should involve the use of many Terraform modules (rather than defining 24 | everything yourself). Taking AWS as an example, this might include VPCs, ASGs, SGs, public/private subnets etc. Each of 25 | these modules should be unit tested using `rspec-terraform` so that the interfaces they expose are well-defined. 26 | 27 | Assembling many individual modules into a cohesive platform should also - ideally - be tested. It's unclear at this 28 | stage how this might look and is yet to be implemented. Something similar to 29 | [`serverspec`](https://github.com/mizzy/serverspec) may work. 30 | 31 | ## Usage 32 | 33 | As [outlined above](#motivation), only simple unit-test type operations are currently supported. 34 | 35 | ### Setup 36 | 37 | You will need Ruby and bundler installed. 38 | 39 | Create a `Gemfile` at the root of your Terraform module and add the following: 40 | 41 | ```ruby 42 | source 'https://rubygems.org' 43 | 44 | gem 'rspec-terraform' 45 | ``` 46 | 47 | Then install the gem: 48 | 49 | ```bash 50 | bundle 51 | ``` 52 | 53 | ### Matchers 54 | 55 | **N.B.** You must set the provider for each test. This is best done in the opening `describe` block: 56 | 57 | ```ruby 58 | describe 'tf-aws-vpc', provider: :aws do 59 | ... 60 | end 61 | ``` 62 | 63 | At present, only the `AWS` provider is available. 64 | 65 | The matchers currently implemented are: 66 | 67 | 1. `require_variables` 68 | 69 | ```ruby 70 | it 'expects the correct variables to be provided' do 71 | expected_variables = %w(vpccidr ec2nameserver region account envname domain) 72 | expect('terraform plan').to require_variables expected_variables 73 | end 74 | ``` 75 | 2. `create_a_plan` 76 | 77 | ```ruby 78 | it 'creates a plan' do 79 | expect('terraform plan -var-file example_variables/test_values.tfvars').to create_a_plan 80 | end 81 | ``` 82 | 83 | ## Contributing 84 | 85 | 1. Fork it ( https://github.com/bsnape/rspec-terraform/fork ) 86 | 2. Create your feature branch (`git checkout -b my-new-feature`) 87 | 3. Commit your changes (`git commit -am 'Add some feature'`) 88 | 4. Push to the branch (`git push origin my-new-feature`) 89 | 5. Create a new Pull Request 90 | --------------------------------------------------------------------------------