Browse files

Add integration test for Rails DNA

The end-to-end integration test will:

  1. create a brand new Rails app, with sample data, and with a
     /users.json endpoint that returns all data from the "users"
     table
  2. install elzar into the Rails app
  3. provision a new EC2 instance
  4. apply (cook) the chef recipes
  5. deploy the Rails app to the instance
  6. curl the /users.json endpoint to verify that nginx and passenger
     are serving the app, and that the app can read from the database and
     return the data
  7. terminate the EC2 instance

To run the integration test:

  1. Create a directory holding the AWS credentials (i.e.,
     aws_config.yml and aws_config.private.yml).
  2. Execute:

       AWS_CONFIG_DIR=/path/to/credentials/dir script/ci_nightly
  • Loading branch information...
1 parent 7d1a98e commit 85373109dde40e89f5f392d95065eab93345c328 Jason Rudolph and Yoko Harada committed Oct 2, 2012
View
4 Gemfile.lock
@@ -10,6 +10,8 @@ PATH
GEM
remote: http://rubygems.org/
specs:
+ bahia (0.7.2)
+ open4 (~> 1.3.0)
builder (3.1.3)
diff-lcs (1.1.3)
excon (0.16.2)
@@ -33,6 +35,7 @@ GEM
net-ssh (2.6.0)
jruby-pageant (>= 1.1.1)
nokogiri (1.5.5)
+ open4 (1.3.0)
rake (0.9.2.2)
rspec (2.11.0)
rspec-core (~> 2.11.0)
@@ -49,6 +52,7 @@ PLATFORMS
ruby
DEPENDENCIES
+ bahia
bundler
elzar!
rake (~> 0.9.2.2)
View
1 elzar.gemspec
@@ -21,5 +21,6 @@ Gem::Specification.new do |s|
s.add_dependency 'slushy', '~> 0.1.3'
s.add_development_dependency 'rake', '~> 0.9.2.2'
s.add_development_dependency 'rspec'
+ s.add_development_dependency 'bahia'
s.add_development_dependency 'bundler'
end
View
14 script/ci_nightly
@@ -0,0 +1,14 @@
+#!/bin/bash
+
+source "$HOME/.rvm/scripts/rvm"
+export CI_RUBY_VERSION="ruby-1.9.3-p194"
+export CI_GEMSET=elzar_nightly
+
+# Tests and app are all running in CI_GEMSET
+export NO_RVM=true
+
+rvm use "$CI_RUBY_VERSION@$CI_GEMSET"
+gem install bundler
+bundle install
+
+bundle exec rspec --tag ci
View
9 spec/fixtures/rails_integration_template/add_root_user.rb
@@ -0,0 +1,9 @@
+class AddRootUser < ActiveRecord::Migration
+ def up
+ User.find_or_create_by_username! 'root'
+ end
+
+ def down
+ raise ActiveRecord::IrreversibleMigration
+ end
+end
View
7 spec/fixtures/rails_integration_template/database.yml
@@ -0,0 +1,7 @@
+production:
+ adapter: postgresql
+ encoding: unicode
+ database: elzar_nightly_app_production
+ pool: 5
+ username: deploy
+ password: d3pl0y-p0stgr3s
View
35 spec/fixtures/rails_integration_template/deploy.rb
@@ -0,0 +1,35 @@
+require 'bundler/capistrano'
+
+default_run_options[:pty] = true
+
+set :application, "elzar_nightly_app"
+set :repository, "/tmp/elzar_nightly_app" # TODO Find a way not to duplicate this path here and inside the spec. Pass as env arg?
+
+set :user, 'deploy'
+set :use_sudo, false
+set :scm, :git
+set :deploy_via, :copy
+set(:deploy_to) { "/var/www/apps/#{application}" }
+set(:server_ip) { ENV['SERVER_IP'] || raise("You must supply SERVER_IP") }
+
+role :web, server_ip
+role :app, server_ip
+role :db, server_ip, :primary => true
+
+
+after 'deploy:update_code', 'deploy:symlink_configs'
+after 'deploy:update_code', 'deploy:migrate'
+
+namespace :deploy do
+ task :start do ; end
+ task :stop do ; end
+ task :restart, :roles => :app, :except => { :no_release => true } do
+ run "#{try_sudo} touch #{File.join(current_path,'tmp','restart.txt')}"
+ end
+
+ task :symlink_configs do
+ shared_configs = File.join(shared_path,'config')
+ release_configs = File.join(release_path,'config')
+ run("ln -nfs #{shared_configs}/database.yml #{release_configs}/database.yml")
+ end
+end
View
19 spec/fixtures/rails_integration_template/template.rb
@@ -0,0 +1,19 @@
+TemplateRoot = File.expand_path '..', __FILE__
+
+gem 'capistrano', :group => 'development'
+gem 'therubyracer', :group => 'assets', :platforms => :ruby
+run 'bundle install'
+run 'bundle exec capify .'
+
+remove_file File.join('config', 'deploy.rb')
+copy_file File.join(TemplateRoot, 'deploy.rb'), File.join('config', 'deploy.rb')
+
+run 'rails generate scaffold user username:string'
+
+next_migration_timestamp = (Time.now + 1)
+migration_filename = next_migration_timestamp.utc.strftime("%Y%m%d%H%M%S") + '_add_root_user.rb'
+copy_file File.join(TemplateRoot, 'add_root_user.rb'), File.join('db', 'migrate', migration_filename)
+
+git :init
+git :add => "."
+git :commit => "-a -m 'Initial commit'"
View
190 spec/integration/rails_spec.rb
@@ -0,0 +1,190 @@
+require 'spec_helper'
+require 'fileutils'
+require 'json'
+
+describe "Rails integration", :ci => true do
+ let(:rails_app) { 'elzar_nightly_app' }
+ let(:path_to_rails_app) { File.join '/tmp', rails_app }
+ let(:server_name) { "Elzar Nightly (rails) - #{Time.now.strftime("%Y-%m-%d %H:%M:%S")}" }
+ let(:aws_config_dir) { ENV['AWS_CONFIG_DIR'] || raise('You must set AWS_CONFIG_DIR to run the integration tests') }
+ let(:instance_info) { { :id => nil, :ip => nil } }
+
+ ######################################################################
+ # Command line helpers
+ ######################################################################
+
+ # wrapper around system
+ def shell(cmd)
+ puts "Executing #{cmd}..."
+ Bundler.clean_system(cmd)
+ abort "Command '#{cmd}' failed" unless $?.success?
+ end
+
+ def elzar(args)
+ puts "Running `elzar #{args}`"
+
+ super
+
+ raise "Error running `elzar #{args}`. Failed with: #{stderr}" unless process.exitstatus == 0
+ end
+
+ def rake(cmd)
+ sh "bundle exec rake #{cmd}"
+ end
+
+ def in_rails_app(&block)
+ pwd = FileUtils.pwd
+ FileUtils.cd path_to_rails_app
+ yield
+ ensure
+ FileUtils.cd pwd
+ end
+
+ ######################################################################
+ # Rails app helpers
+ ######################################################################
+
+ def create_new_rails_app
+ shell "gem install rails"
+ FileUtils.rm_rf(path_to_rails_app)
+ rails_template = File.expand_path('../../fixtures/rails_integration_template/template.rb', __FILE__)
+ shell %Q{rails new "#{path_to_rails_app}" -d postgresql -m "#{rails_template}"}
+ end
+
+ def configure_elzar
+ in_rails_app do
+ rewrite_json('provision/dna.json') do |dna|
+ dna['rails_app']['name'] = rails_app
+ end
+ end
+ end
+
+ def rewrite_json(path_to_json, &block)
+ json = JSON.parse File.read(path_to_json)
+ yield json
+ File.open(path_to_json, 'w') { |f| f << JSON.generate(json) }
+ end
+
+ ######################################################################
+ # AWS helpers
+ ######################################################################
+
+ def aws_config
+ @aws_config ||= Elzar::AwsConfig.load_configs(aws_config_dir)
+ end
+
+ def fog
+ @fog ||= Fog::Compute.new(aws_config['aws_credentials'].merge(:provider => 'AWS'))
+ end
+
+ def server(instance_id)
+ fog.servers.get(instance_id).tap do |s|
+ s.private_key = aws_config['server']['private_key']
+ end
+ end
+
+ def ssh(server, cmd)
+ job = nil
+ capture_stdout { job = server.ssh(cmd).first }
+ job
+ end
+
+ def put_database_config_on_server(server)
+ shared_path = "/var/www/apps/#{rails_app}/shared/config"
+ path_to_db_config = File.expand_path('../../fixtures/rails_integration_template/database.yml', __FILE__)
+ server.scp path_to_db_config, "/home/ubuntu/database.yml"
+
+ ssh server, "sudo mkdir -p #{shared_path}"
+ ssh server, "sudo mv ~/database.yml #{shared_path}/database.yml"
+ ssh server, "sudo chown -R deploy:deploy #{shared_path}"
+ end
+
+ def destroy_instance(instance_id)
+ in_rails_app do
+ elzar "destroy \"#{instance_id}\" --aws_config_dir=#{aws_config_dir}"
+ end
+ end
+
+ def capture_instance_details(output)
+ id = output.match(/Instance ID: (.+)$/i)[1]
+ ip = output.match(/Instance IP: (.+)$/i)[1]
+ [id, ip]
+ end
+
+ ######################################################################
+ # Assertion helpers
+ ######################################################################
+
+ # Returns true if the command gives zero exit status, false for non zero exit status.
+ def execute_local_command(cmd)
+ Bundler.clean_system(cmd)
+ end
+
+ # Returns true if the command gives zero exit status, false for non zero exit status.
+ def execute_remote_command(server, cmd)
+ ssh(server, cmd).status == 0
+ end
+
+ def assert_state_after_init
+ in_rails_app do
+ execute_local_command('ls provision > /dev/null').should == true
+ execute_local_command('grep -q rails provision/dna.json').should == true
+ end
+ end
+
+ def assert_state_after_preheat(server)
+ execute_remote_command(server, 'gem list | grep chef').should == true
+ end
+
+ def assert_state_after_cook(server)
+ execute_remote_command(server, '/opt/relevance-ruby/bin/ruby -v | grep 1\.9\.3').should == true
+ execute_remote_command(server, 'sudo service postgresql status').should == true
+ execute_remote_command(server, 'sudo service nginx status').should == true
+ execute_remote_command(server, 'ls /home/deploy').should == true
+ end
+
+ def assert_state_after_deploy(server_ip)
+ execute_local_command(%Q{curl -sL -w '%{http_code}' #{server_ip} -o /dev/null | grep 200}).should == true
+ execute_local_command(%Q{curl -s #{server_ip}/users.json | grep '"username":"root"'}).should == true
+ end
+
+ it 'works' do
+ create_new_rails_app
+
+ in_rails_app do
+ elzar "init --dna=rails"
+ end
+
+ assert_state_after_init
+
+ in_rails_app do
+ configure_elzar
+ elzar %Q{preheat "#{server_name}" --aws_config_dir=#{aws_config_dir}}
+ instance_info[:id], instance_info[:ip] = capture_instance_details(stdout)
+ end
+
+ instance_info[:id].should_not == nil
+ instance_info[:ip].should_not == nil
+ server = server(instance_info[:id])
+
+ assert_state_after_preheat(server)
+
+ in_rails_app do
+ elzar "cook \"#{instance_info[:id]}\" --aws_config_dir=#{aws_config_dir}"
+ end
+
+ assert_state_after_cook(server)
+
+ in_rails_app do
+ shell %Q{SERVER_IP="#{instance_info[:ip]}" cap deploy:setup}
+ put_database_config_on_server(server)
+ shell %Q{SERVER_IP="#{instance_info[:ip]}" cap deploy}
+ end
+
+ assert_state_after_deploy(instance_info[:ip])
+ end
+
+ after(:each) do
+ destroy_instance(instance_info[:id]) if instance_info[:id]
+ end
+end
View
11 spec/spec_helper.rb
@@ -1,10 +1,21 @@
require 'elzar'
+require 'bahia'
+
+Dir['spec/support/**/*.rb'].each do |support_file|
+ require File.expand_path(support_file)
+end
+
+Bahia.project_directory = File.expand_path('../..', __FILE__)
RSpec.configure do |config|
config.filter_run_excluding :disabled => true
+ config.filter_run_excluding :ci => true
config.run_all_when_everything_filtered = true
config.alias_example_to :fit, :focused => true
config.alias_example_to :xit, :disabled => true
config.alias_example_to :they
+
+ config.include Bahia, :ci => true
+ config.include ShellInteractionHelpers, :ci => true
end
View
33 spec/support/shell_interaction_helpers.rb
@@ -0,0 +1,33 @@
+module ShellInteractionHelpers
+ def capture_stderr(&block)
+ original_stderr = $stderr
+ $stderr = fake = StringIO.new
+ begin
+ yield
+ ensure
+ $stderr = original_stderr
+ end
+ fake.string
+ end
+
+ def capture_stdout(&block)
+ original_stdout = $stdout
+ $stdout = fake = StringIO.new
+ begin
+ yield
+ ensure
+ $stdout = original_stdout
+ end
+ fake.string
+ end
+
+ # wrapper around raise_error that captures stderr
+ def should_abort_with(msg)
+ capture_stderr do
+ expect do
+ yield
+ end.to raise_error SystemExit, msg
+ end
+ end
+end
+

0 comments on commit 8537310

Please sign in to comment.