Tuesday, December 15, 2009

Authenticate your Authlogic users in Rails Metal

Update 2010-03-17: Don't fail in test mode if we can't find a user

I haven't been able to find any information on authenticating users in Rails Metal controllers. So I took a look at what was available in the session and worked with that. I'm not an expert on Authlogic (I did write an Authlogic Add-On a while ago) but this seems like a good solution. Let me know if you know of any problems with this approach.

I added a method to UserSession (or add it to whatever class you use for your sessions) that takes the Metal env and checks the authentication parameters and returns the user record. I don't bother with updating the user's last_request_at attributes or anything. I had to bypass the Authlogic session entirely because Authlogic needs to be activated with a controller before you can use the session stuff. I considered dummying the controller by including authlogic/test_case but I don't know enough about what the dummy controller does regarding authentication to go this route.

And the code...

Here's the Metal "controller":

A couple neat things in the Metal controller are the use of ActionController::Request.new(env) so we have access to request.remote_ip and other helper methods this class provides.

The part where I validate the user is:

Tuesday, December 8, 2009

Silence the Rails logger as well as the SQL logs

It's one thing to turn off logging in Rails, it's another to prevent SQL statements from appearing in your logs.

I've written a Rake helper to silence both. You can adapt it for other uses, but I think it's mostly relevant in Rake tasks.

# lib/rake_helpers.rb
module Rake
  module Helpers
    # Silence the Rails logger as well as the SQL logger
    # Call with a block e.g
    # silent { my block code }
    def silent
      ActiveRecord::ConnectionAdapters::AbstractAdapter.class_eval do
        alias_method :original_log_info, :log_info
        def silent_log_info(*args); end
        alias_method :log_info, :silent_log_info
      end
      @@old_logger = ActiveRecord::Base.logger
      ActiveRecord::Base.logger = Class.new { def method_missing(*args); end; }.new
      yield
      ActiveRecord::Base.logger = @@old_logger
      ActiveRecord::ConnectionAdapters::AbstractAdapter.class_eval do
        alias_method :log_info, :original_log_info
      end 
    end
  end
end
And sample usage:
# lib/tasks/db.rake
require 'rake_helpers'
include Rake::Helpers

namespace :db do
  namespace :update do

    desc "Update counter caches"
    task :counters => :environment do
      silent do
        ActiveRecord::Base.transaction do
          [Artist, Album].each do |model| 
            model.find_in_batches(:batch_size => 5000) do |batch|
              batch.each { |record| record.update_counts }
            end
          end
        end
      end
    end
  end
end

Friday, December 4, 2009

Rails Metal real-world example with RSpec integration test

There's not that much information out there about Rails Metal, and even less about how to test Metal classes using RSpec. If I had time I would create a new RSpec ExampleGroup with a lot of the goodness of the ControllerExampleGroup, but alas I don't.

So integration tests will have to do. I had problems with using response.should be_success. The docs say it exists, but it doesn't for me. YMMV.

Anyways, here is my Metal class along with the integration test:

Thursday, December 3, 2009

Bash script to easily check and switch between Rails environments. Usage: renv [p|d|t|s]

When I'm on our deployment server I'm constantly checking the RAILS_ENV environment variable to make sure that I don't accidentally do something on the production database when I meant to do it on the staging database. (Incidentally I’ve added some stuff to the Rakefile to guard against such a thing: Rakefile: Prevent destructive rake tasks in the production environment.). So I created this handy Bash function to check and switch between your Rails environments easily.

Sample usage:

$ renv
RAILS_ENV is unset
$ renv p
RAILS_ENV set to production
$ renv d
RAILS_ENV set to development
$ renv s
RAILS_ENV set to staging
$ renv t
RAILS_ENV set to test
$ renv
RAILS_ENV is test

And the code:

# Put this in your ~/.profile
function renv() {
  rails_env_p="production" 
  rails_env_d="development" 
  rails_env_s="staging" 
  rails_env_t="test" 

  if [ $1 ];then
    eval 'rails_env=${rails_env_'"${1}"'}' 
    export RAILS_ENV=$rails_env
    echo "RAILS_ENV set to $RAILS_ENV" 
  elif [ $RAILS_ENV ];then
    echo "RAILS_ENV is $RAILS_ENV" 
  else
    echo "RAILS_ENV is unset" 
  fi
}

Rakefile: Prevent destructive rake tasks in the production environment.

We are using the same server for our staging and production deploys. The problem is that the default RAILS_ENV is set to production so when I go to the staging app and start messing about with rake db:seed (or worse, rake db:reset) I am almost always a hair's breadth away from imminent death.

(Here's a handy bash script I wrote to easily switch between your Rails environments.)

So I decided to do something about it and protect destructive or database altering rake tasks from being run in the production environment. I really like this approach and I think I'll use it in future projects regardless of the setup. The fact is you only do a rake db:reset in production once, during the cold deploy of your app. It allows rake db:migrate and its variants, but not rake db:migrate:reset. Look at the code for the full list of restricted rake tasks. The tasks in PROTECT_SOME are matched explicitly, PROTECT_ALL restricts all variants of that task.

This shows the warning message and confirmation that appear. If you know what you are doing, or you want to automate things with Capistrano and need to run one of the protected tasks just pass FORCE=true to the task.

[deployer@rails1 ~/rails_apps/rails1.creagency.com.au/current] echo $RAILS_ENV
production
[deployer@rails1 ~/rails_apps/rails1.creagency.com.au/current] rake db:reset
(in /www/rails_apps/rails1.creagency.com.au/releases/20091204033227)
****************************************************************************
* WARNING! You are in the PRODUCTION environment and are running a Rake task 
* that will DESTROY your PRODUCTION database!
* 
* If you know what you are doing you can run this task with FORCE=true to
* prevent this message appearing.
****************************************************************************

Are you sure? (Yes|No) [No]

Quitting.
[deployer@rails1 ~/rails_apps/rails1.creagency.com.au/current] 
And here is the code. Just add it to the bottom of your application's Rakefile:

Solved: Capistro deploy fails with 'Permission denied (publickey).' but you know you have your permissions setup correctly

I spent a couple hours scratching me head one day when I couldn't deploy to the production server, but I could a little while ago.

The Capistrano deployment recipe had been changed to forward the ssh agent (which means that my local ssh key is the one that will be used by the deployment server to checkout the updated code from Git):

  ssh_options[:forward_agent] = true
Capistrano was complaining:
    servers: ["10.5.23.203"]
    [10.5.23.203] executing command
 ** [10.5.23.203 :: out] Permission denied (publickey).
 ** [10.5.23.203 :: out] fatal: The remote end hung up unexpectedly
The solution was just to add my SSH key to my local SSH agent:
$ ssh-add
Simple enough. Turns out you have to do that every time you restart your machine tho, which is a bit annoying. Any way to avoid that?

Cheers, Karl

Wednesday, December 2, 2009

Git: how to keep the master branch tidy, even if you've been working on it

In order to keep the master branch clean we want to avoid merges and "noisy" commits.

As you probably know, the best thing to do is to develop in a branch and rebase against master often (never rebase a remote branch). This ensures that your changes are always the latest changes applied to the index. And because they're applied on top of the index, when you merge them into the master branch and push, it's just a fast-forward update and you will never have to do a merge.

But what happens if you've been working on the master branch for a while? Is it too late? I just encountered this myself, and here's how you can keep the master merge-free in this situation.

On master

git fetch # have there been changes to master since you last fetched/pulled?  if not, you can just commit and push, otherwise...

git commit -a -m "awesome message here" # commit your changes
git branch wip           # create a new branch with your changes
git reset --hard HEAD~1  # reset the master branch to before your commit(s)
git checkout wip
continue working...
git fetch origin master
git rebase origin/master # rebase your changes on top of master often
all done...
git rebase -i origin/master # preferably squash your commits
git checkout master
git pull                 # make sure master has the latest code
git merge wip            # apply your fast-forward changes to the index
git push                 # quick before someone else does!

Can this process be shortened? Let me know if you have ideas.

Here are some excellent articles explaining some Git best-practices. There is some excellent info here, even from Linus himself!

Let me know if you have good Git articles. I'm still learning about Git, but now that I'm getting my head around it, it's getting better and better!

Karl