Monday, July 5, 2010

Install a Rails App's dependencies where 'rake gems:install' fails.

Using rake gems:install to install the gem dependencies of your Rails 2 app just doesn't seem to work. There are a number of problems you may encounter:

Problem 1: It depends on Rails.
You have to manually install Rails before you can use rake gems:install. Ok, so this might not be that big of a deal, but consider this scenario:

  • Your app is running in production on rails 2.3.4.
  • You upgrade your app to rails 2.3.5, including the RAILS_GEM_VERSION.
  • You deploy your app.
  • Your deployment script calls rake gems:install to keep the dependencies up to date with each new release.
  • That fails with something that looks like:
[scottwb@test]% rake gems:install
(in /home/scottwb/src/my_rails_app)
Missing the Rails 2.3.5 gem. Please `gem install -v=2.3.5 rails`, update your RAILS_GEM_VERSION setting in config/environment.rb for the Rails version you do have installed, or comment out RAILS_GEM_VERSION to use the latest version installed.
view raw gistfile1.txt hosted with ❤ by GitHub

Problem 2: Sometimes, for some reason, it just doesn't work.
Sometimes (most times) you add a new config.gem command to environment.rb. The next deployment runs rake gems:install and it fails with the complaint that you are missing the very gem you are hoping it will install for you. For example, in a working Rails 2.3.5 project, add this line to environment.rb:

config.gem 'sanitize', :version => '1.2.1'

Then, run rake gems:install, and it fails with a "Missing these required gems:" message, e.g.:

[scottwb@test]% rake gems:install
(in /home/scottwb/src/my_rails_app)
Missing these required gems:
sanitize = 1.2.1
You're running:
ruby 1.8.6.383 at /usr/bin/ruby
rubygems 1.3.5 at /home/scottwb/.gem/ruby/1.8, /usr/lib/ruby/gems/1.8
Run `rake gems:install` to install the missing gems.
view raw gistfile1.txt hosted with ❤ by GitHub

WTF? It's telling me to run the command I just ran, to install the missing gem that's preventing that command from running.


Problem 3: It depends on your Rails environment.
This can create a circular dependency where some file (particularly vendored plugins/gems) can require a gem to be loaded before the task to install it runs. For example, consider that some file in your vendor directory does this:

require 'gdata'

Then naturally, you add this to environment.rb:

config.gem 'gdata'

The next time you run rake gems:install, it will fail with a "no such file to load" error, e.g.:

[scottwb@test]% rake gems:install
(in /home/scottwb/src/my_rails_app)
rake aborted!
no such file to load -- gdata
/home/scottwb/src/ranger_site/Rakefile:10
(See full trace by running task with --trace)
view raw gistfile1.txt hosted with ❤ by GitHub


The Solution: rails_gem_install
I have created a new tool called rails_gem_install to help alleviate this problem. Use this instead of rake gems:install, and you should be much more successful at getting all your Rails app's dependencies installed without manual intervention.

To install:

gem install rails_gem_install

To use:

cd my_rails_app
RAILS_ENV=production rails_gem_install

This will install all the gems required to run your app in the production environment, including Rails. Replace rake gems:install in your deployment scripts with rails_gem_install, and as you change config.gem requirements, those gems will be properly installed.

See the github project page for more details.


How It Works
The main principle behind this tool is that it does not depend on Rails. It creates its own module named Rails that provides some of the functionality from the real Rails that it needs, up until the point that Rails is installed. Then, it only loads some very specific parts of Rails that implement the gem dependency and installation mechanisms, without actually running the app's Rails::Initializer and loading all of its environment, plugins, etc.

First, it runs a simple rake -T to see if it complains about Rails missing. It parses the output of this, and if necessary, installs the indicated version of Rails.

Next, it parses out the config.gem statements and uses those to ensure all the listed gems requirements are met, and installs those that aren't. It does this without actually loading Rails or the app environment. This carries us most of the way.

Finally, it runs the rake gems command and parses its output to detect all the kinds of errors described above and install the corresponding gems. This step is repeated until rake gems does not complain anymore.

Thursday, July 1, 2010

Hudson CI behind an Nginx Reverse Proxy with SSL

Here is a quick example nginx configuration to reverse proxy on an HTTPS virtual host to a Hudson CI server running on localhost. When I first tried to do this, the management page displays an error about the configuration being wrong. There are instructions for Running Hudson behind Apache that were helpful, and this email thread that seems to suggest terminating SSL at Hudson, not at the reverse proxy. Well, after a bit of tinkering, I worked out this configuration for nginx that worked out great:


# Nginx config for Hudson CI behind a virtual host with SSL.
# Replace hudson.example.com with your domain name.
# Upstream Hudson server, e.g.: on port 3001
upstream hudson {
server localhost:3001
}
# Redirect all HTTP requests to HTTPS.
server {
listen 80;
server_name hudson.example.com;
location / {
rewrite ^ https://hudson.example.com$request_uri? permanent;
}
}
# Proxy HTTPS requests on hudson.example.com to localhost Hudson server.
server {
listen 443;
server_name hudson.example.com;
ssl_on;
ssl_certificate /etc/pki/tls/certs/hudson_example_com.pem;
ssl_certificate_key /etc/pki/tls/certs/hudson_example_com.pem;
ssl_session_cache shared:SSL:1m;
ssl_session_timeout 5m;
# Only allow GET, HEAD, and POST requests.
if ($request_method !~ ^(GET|HEAD|POST)$ ) {
return 444;
}
location / {
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto https;
proxy_set_header Host $http_host;
proxy_next_upstream error;
proxy_pass http://hudson;
proxy_redirect http://hudson.example.com/ https://hudson.example.com/;
}
}