Atalanta / cucumber-chef

Framework for test-driven infrastructure development
http://cucumber-chef.org
Apache License 2.0
265 stars 55 forks source link

chef, knife, steps and you #94

Closed zpatten closed 11 years ago

zpatten commented 11 years ago

So until recently all of the internal interactions with the chef-server in Cucumber-Chef have been via direct calls to chef code. We require in the chef libraries, and then call their methods. This has worked pretty well. For just about anything one can look at the knife source for a command you wanted to turn into a cuke step. This was ultimately very inefficient. You had to write supporting methods which wrapped the chef code up in the helpers libraries. Then you finally had some nice high level methods for use in making your steps.

This of course bite us a few times. As the chef internals change, Cucumber-Chef will suddenly explode in spectacular and often random ways when calling those changed chef internals for obvious reasons. This was very annoying and wasted time. First we get a ton of messages, you know something like: "Oh hey; your software just randomly stopped working; great job moron!" and second I have to perform immediate triage and get a "fix" out. I originally viewed calling the chef internals directly as having more control; I feel very wrong now.

I decided to take out the refactor wrench and get to work; doubly inspired by the changes coming in Vagrant and when you look at programs like git which already have these types of flags for interacting with other programs; it's hard to not want to go this way.

With these changes, I was able to remove the helper methods completely and embed the code right into the step by throwing away the code which called the chef internals, replacing it with a root level (in the namespace that is) helper for executing knife commands easily from a step.

Take this code from the chef_server.rb helper:

  def load_role(role, role_path)
    expanded_role_path = File.expand_path(role_path)
    if !File.exists?(expanded_role_path)
      raise "Role path, '#{expanded_role_path}', does not exist!"
    end
    ::Chef::Config[:role_path] = expanded_role_path
    role = ::Chef::Role.from_disk(role)
    role.save
    logger.info { "Updated chef role '#{role}' from '#{role_path}'." }
  end

It's counterpart in the chef_steps.rb step file:

And /^the following roles have been updated:$/ do |table|
  table.hashes.each do |entry|
    load_role(entry['role'], entry['role_path'])
  end
end

While this code isn't horrid; it's not ideal like I said before.

Now you just have to put the command line params together, well for the most part at least.

So now the above code can turn into this:

And /^the following (role|roles) (has|have) been (updated|uploaded):$/ do |ignore0, ignore1, ignore2, table|
  table.hashes.each do |entry|
    role = entry['role']
    role_path = entry['role_path']

    if File.extname(role).empty?
      Dir.glob(File.join(role_path, "#{role}.*")).each do |role_file|
        $test_lab.knife_cli(%Q{role from file #{role_file}}, :silence => true)
      end
    else
      $test_lab.knife_cli(%Q{role from file #{File.join(role_path, role)}}, :silence => true)
    end
  end
end

A lot more compact; and it will only break if Opscode goes out and rearranges the knife command line options, which I feel is not likely to happen anytime soon (knocks on wood).

This also opens the door to easily add more steps rapidly with less upfront research required, at least regarding knife functions.

zpatten commented 11 years ago

I should also note; one could add the command line option to have knife return json for example. If you capture the return value from the knife_cli call, it has an output attribute that will contain STDOUT/STDERR combined output from your command. You could then parse this json and take other actions based on the results for example.