Chef order of execution

chef

I have some code that sets node attributes based on a search. I then want to use those attributes in a template. It appears the template is compiling before my code that figures out the values. So it takes 2 runs of chef-client to get to the state I want

if !node['foo']
  search(:node, "recipes:bla").each do |bla|
    if bla['bla'] > node['foo']
      node['foo'] = bla['bla']
    end
  end
end

template "/tm/foo" do
  source "foo"
end

I've tried putting this code in the recipe before and after the template, and in the attributes file. I don't think search has an action or else I could try using .run_action() on it.

Is there any way to get the value of node['foo'] to be set so that it will be used in the template?

EDIT: Clarification

cook_a/attributes/default.rb

default['cook_a']['val_1'] = node['someval']

cook_a/recipes/default.rb

template "/etc/cook_a.conf" do
  source "cook_a.conf.erb"
end

cook_a/templates/default/cook_a.conf.erb

some_var = <% node['cook_a']['val_1'] %>

Now over in another cookbook I'm overriding that value

coob_b/recipes/default.rb

node.set['someval'] = "foo"
include "cook_a"

But now its too late to change node['cook_a']['val_1'] and so the template writes the original value of node['someval'] during the first run. During the second run I get the correct value.

I'm reluctant to be setting node['cook_a']['val_1'] as that name may change and I'm trying to abstract away from the details of cook_a.

Best Answer

This is a little hard to understand, due to the overuse of bla, and the lack of what the template is using.

Assuming the template has a statement in it similar to:

somevalue = <%= node['foo'] %>

And that you are not getting the overriden value of node['foo'] in the template (which seems to be desired as bla['bla']?) There's a couple of ways I would set about this.

  1. Chef should actually be warning you when attempting to replace the value of node['foo'] in a recipe with no attribute precedence level.

    Chef::Exceptions::ImmutableAttributeModification: Node attributes are read-only
    when you do not specify which precedence level to set. To set an attribute use
    code like `node.default["key"] = "value"'
    

    So in your recipe you could change that approach to use node.default['foo'] = ... to set the result at an explicit attribute level.

  2. Expand the node attribute to a Ruby variable, and pass that to the template explicitly, to prevent overrides and compiletime vs rendertime conflicts.

    This would look something like this:

    if !node['foo']
      search(:node, "recipes:bla").each do |bla|
        if bla['bla'] > node['foo']
          node_foo = bla['bla']
        end
      end
    end
    
    template "/tmp/foo" do
      source "foo"
      variables(
        node_foo: node_foo || node['foo']
      )
    end
    

    And the template would change to look like:

    somevalue = <%= @node_foo %>
    

    There are other ways to write the conditional logic, but overall it means that we expand node attributes in the recipe, and pass along only the variables we want to the template, not more node attributes.

Finally, something that concerns me with this recipe is that the search is conditional on node['foo'] returning a nil value, and then the same known nil value is used as a comparison within the assignment block. I don't think this does much, as if entering the block, it will always be nil, therefore the comparison doesn't do much.

EDIT:

Post-question clarification, I believe you are running into the "chicken and egg" problem with compiletime vs rendertime attribute evaluation, as previously stated.

The only reason the correct value is being retrieved during the second run is due to the node.set action saving the attribute in the node object itself on Chef Server, which means that setting anything in attributes or overrides anywhere later on won't matter, since a node attribute typically wins these, as it is most explicit.

Be aware about how these are set and how to remove them - they can cause confusion if abused. Read up on Attribute Precedence.

On to your attribute.

The attributes files are loaded in order of dependencies and lexical sort, so cook_b will always be evaluated after cook_a, and you are probably better off moving the node.set to cook_b's attributes/default.rb as an override attribute, and ensure the attributes files are loaded in correct order.

You might also want to try forcing the attribute to be lazy evaluated when rendering the template, like so:

cook_a/recipes/default.rb

template "/etc/cook_a.conf" do
  source "cook_a.conf.erb"
  variables lazy {
    { some_var: node['cook_a']['val_1'] }
  }
end

cook_a/templates/default/cook_a.conf.erb

some_var = <%= @some_var %>

More on Lazy Attribute Evaluation here.