Babushka: test-driven sysadmin.

How deps work

Dep is short for dependency – one single piece of a larger task. A little nugget of code that does just one thing, and does it right.

Deps hook together by requiring each other. When you run babushka you specify a dep as an entry point, and babushka processes it and its requirements as a tree.

In code

Here’s a babushka dep, at its most generic.

dep 'name', :argument do
  requires 'other deps'.with('args'), 'whatever they might be'
  met? {
    # is this dependency already met?
  }
  meet {
    # this code gets run if it isn't.
  }
end

met? should be an idempotent block of code that returns true if the dep is “met” – that is, if its job is already done.

meet shouldn’t check anything at all: it should do the dep’s job unconditionally. Its return value is ignored.

The interaction between met? and meet defines babushka.

met? / meet / met?

When a dep is run, its met? block is called. If it returns true, then the dep’s job is done.

If it’s unmet, though, then meet is run (and its return value ignored), and then met? is run again to see if running meet caused the dep to become met.

The idea is that met? and meet are complementary: met?’s job is checking whether meet has done its job properly.

I like to think of an unmet dep’s met? block as a failing test, and meet as the code that makes that test pass.

Take the rubygems dep as an example:

Things like rubygems aren’t hard to install on their own, but with babushka it’s fast and predictable: the job is done just right, every time.

What, not how

This implies an important part of the design: the met? block shouldn’t just directly check that meet did a piece of work; that would be trivial repetition. Instead, A good test checks the result of the work. For example:

In short, met? should check meet’s intent, not its implementation: it should check the what, not the how.

Requirements

Another point to note is that when you’re writing a dep, you shouldn’t have to think very much outside it – the dep should be context-free, much like a function should encapsulate a piece of logic without regard for the context in which it’s called.

As long as the correct requirements are specified, babushka will sort out the context for you, and you can write each dep independently, passing the required arguments around.

For example, a dep assigning database permissions requires an existing database, and so should require a dep that checks for it, creating it if necessary.

dep 'db access', :db_name do
  requires 'db exists'.with(db_name)
  met? {
    # Check if we can read something from the database
  }
  meet {
    # Assign the correct permissions to the database
  }
end

With that requirement, the met? and meet logic in ‘db exists’ can just assume that the database already exists. We don’t need any “if db_exists” conditionals in this dep: that’s not relevant in this context, thanks to the requirement. There’s some more on context in the section on writing deps.