How to lazily evaluate an arbitrary variable with Chef

Patrick M picture Patrick M · Dec 16, 2013 · Viewed 22.5k times · Source

I'm writing a Chef recipe to install our application code and execute it. The recipe needs to be particular about the directory this code ends up in (for running templates, setting log forwarding etc.). Thus the directory itself pops up in a lot of places in different recipes.

I am trying to fetch/define a variable so I can re-use it in my resource block with string interpolation. This is pretty straightforward:

home = node['etc']['passwd'][node['nodejs']['user']]['dir']

With an example usage being to run npm install while telling it to plunk the repo downloads in the home directory, like this:

execute "npm install" do
  command "npm install #{prefix}#{app} --prefix #{home}"
end

Except that the first block which defines the home variable will run at compile time. On a fresh server, where my nodejs user account may not exist yet, this is a problem, giving a

NoMethodError undefined method '[]' for nil:NilClass

I have a few workarounds, but I would like a specific solution to make the home variable only be fetched at recipe execute time, not compile time.


Workaround 1

Dynamically evaluate the home variable inside a ruby block, like so:

ruby_block "fetch home dir" do
  block do
    home = node['etc']['passwd'][node['nodejs']['user']]['dir']
  end
end

This does not seem to actually work, giving a NoMethodError undefined method home for Chef::Resource::Directory when you try to do something like this:

directory ".npm" do
  path "#{home}/.npm"
end

I feel like I must be doing something wrong here.

Workaround 2

Lazily evaluate a parameter on every single resource that needs it. So instead do this:

directory ".npm" do
  path lazy "#{node['etc']['passwd'][node['nodejs']['user']]['dir']}/.npm"
end

But it would be really great to just have to maintain that line of code once, store it in a variable and be done with it.

Workaround 3

Create the user at compile time. This of course works, using the notify trick linked here, like this:

u = user node['nodejs']['user'] do
  comment "The #{node['nodejs']['user']} is the user we want all our nodejs apps will run under."
  username node['nodejs']['user']
  home "/home/#{node['nodejs']['user']}"
end

u.run_action(:create)

This solves my problem exactly, but there are other cases where I can imagine wanting the ability to delay evaluation of a variable, so I let my question stand.

What I would Like

I would really like to be able to do

home lazy = node['etc']['passwd'][node['nodejs']['user']]['dir']

But that's not legal syntax, giving NameError Cannot find a resource for home on ubuntu version 13.10 (which is an odd syntax error, but whatever). Is there a legal way to accomplish this?

Answer

thoughtcroft picture thoughtcroft · Dec 19, 2013

I haven't tested this particular code but I have done something similar in cookbooks and used a lambda to delay evaluation as follows:

home = lambda {node['etc']['passwd'][node['nodejs']['user']]['dir']}

execute "npm install" do
  command "npm install #{prefix}#{app} --prefix #{home.call}"
end