How to lazily evaluate an arbitrary variable with Chef
Asked Answered
E

3

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?

Escalate answered 16/12, 2013 at 20:55 Comment(6)
try home = lazy {node['etc']['passwd'][node['nodejs']['user']]['dir']}Antinode
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.Antinode
@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 #<Chef::DelayedEvaluator:0x000000027ac400@/var/chef/cache/cookbooks... etc. lambda does something similar: <Proc:0x00000002847270@. I can't find any examples on how to use DelayedEvaluator in that type of syntax. Any ideas?Escalate
home.call. @thoughtcroft answer is actually proposing the same, just with lambda and not Proc.Antinode
@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?Escalate
The old Chef Wiki link to the "notify trick" has long been broken. Here is a working one from Internet Archive for those still wishing to reference this material in the present. Another Blog Post here details another example of this techniqueCaoutchouc
C
20

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
Conventioneer answered 19/12, 2013 at 0:51 Comment(2)
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.Escalate
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.Conventioneer
F
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.

Freedwoman answered 17/6, 2014 at 7:22 Comment(0)
S
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.

Sparky answered 24/3, 2016 at 12:49 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.