Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to lazily evaluate an arbitrary variable with Chef

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?

like image 473
Patrick M Avatar asked Dec 16 '13 20:12

Patrick M


People also ask

What is guard in chef?

Chef-Guard is a feature rich Chef add-on that protects your Chef server from untested and uncommitted (i.e. potentially dangerous) cookbooks by running several validations and checks during the cookbook upload process.

What are phases of Chef execution?

There are two stages to a chef run though. A compile phase, to organise what resources need to be run and resolve all variables. Then a run phase where each resource is actually executed. You can write recipes that trigger actions during the compile phase which may make it seem out of order.

What are the resources in chef?

Chef resource represents a piece of the operating system at its desired state. It is a statement of configuration policy that describes the desired state of a node to which one wants to take the current configuration to using resource providers.


2 Answers

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
like image 187
bainsworld Avatar answered Nov 02 '22 13:11

bainsworld


For ruby_block, any variables within the block will need to be global as anything defined within the block is local to it.

You can't use a lambda for delayed execution in a library, so ruby_block works well in this case.

like image 28
Oscar Barrett Avatar answered Nov 02 '22 11:11

Oscar Barrett