15

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?

Mark O'Connor
  • 76,015
  • 10
  • 139
  • 185
Patrick M
  • 10,547
  • 9
  • 68
  • 101
  • try `home = lazy {node['etc']['passwd'][node['nodejs']['user']]['dir']}` – Draco Ater Dec 17 '13 at 08:41
  • 1
    Or `home = DelayedEvaluator.new {node['etc']['passwd'][node['nodejs']['user']]['dir']}` or `lambda {node['etc']['passwd'][node['nodejs']['user']]['dir']}` if `lazy` is not available in top scope. – Draco Ater Dec 17 '13 at 08:48
  • @DracoAter thanks for the suggestions. The `home = lazy {` syntax did not compile. The `home = DelayedEvaluator.new` syntax compiles and runs, but it needs some accessor to get the string value out. If I just do `#{home}`, it shows a `tostring` looking value of the reference/class, like `# – Patrick M Dec 18 '13 at 21:32
  • 1
    `home.call`. @thoughtcroft answer is actually proposing the same, just with lambda and not Proc. – Draco Ater Dec 19 '13 at 07:59
  • @DracoAter yep, it seems to work. I was saying above that if you _don't_ do `lambda.call`, the `#{ }` syntax displays it as a `Proc`. I guess Proc is the class name lambda resolves to in Ruby? – Patrick M Dec 19 '13 at 21:27
  • The old Chef Wiki link to the "notify trick" has long been broken. [Here is a working one from Internet Archive](https://web.archive.org/web/20120913065512/http://wiki.opscode.com/display/chef/Evaluate+and+Run+Resources+at+Compile+Time) for those still wishing to reference this material in the present. Another [Blog Post here details another example of this technique](https://web.archive.org/web/20210322005506/http://jinsunspace.blogspot.com/2013/01/ruby-evaluate-and-run-resources-at.html) – TrinitronX Mar 22 '21 at 00:59

3 Answers3

19

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
bainsworld
  • 316
  • 4
  • 8
  • This works exactly as I want it to. I haven't yet verified the delayed execution of it... If you were to add a separate `home = home.call`, would that fire at execution time or compilation time? Just curious, because sprinkling in `home.call` gets a little cumbersome and thus is more error prone if you're not just find/replacing. – Patrick M Dec 19 '13 at 21:26
  • If that code is in normal ruby outside of a resource, it will be evaluated at compile time ie during recipe evaluation. You could write a proper library function which could be called from other recipes. As it stands, the lambda has to be defined in every recipe you want to do this. – bainsworld Feb 05 '14 at 22:39
1

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.

Oscar Barrett
  • 3,135
  • 31
  • 36
1

@thoughtcroft answer not working for me at chef-client 12.8.1

In this cases I place needed code into custom resource and call it with lazy attributes.

mycookbook_myresource "name" do
  attribute lazy { myvar }
end

Not elegant solution but it works for me.

e.aktec
  • 11
  • 2