Pergunta

I read about Chef wrapper cookbook and now I'm try to refactor some kitchen roles in role cookbooks. So for instance I have a lamp role cookbook which does something like this:

# This is coming from another cookbook
include_recipe 'mysql_role::default'
# Install and tune apache2
include_recipe 'lamp_role::apache2'
# Install php and usual modules
include_recipe 'lamp_role::php'
include_recipe 'lamp_role::php_modules'

Included recipes may have some application logic but they usually end with inclusion of official cookbooks

# in lamp_role::php recipe
include_recipe 'php'

Now in another recipe I'd like to check if the official php::default recipe has been included somewhere, I used to do that with one of these two syntaxes:

# This is true for recipes included both in roles and in run_list
include_recipe 'newrelic::php_agent' if node.recipes.include?('php')
# This works only for runlist recipes
include_recipe 'newrelic::php_agent' if node.recipe?('php')

Is there any way to check if a recipe has been included with include_recipe somewhere else?

P.S. I'm using chef-solo to deploy.

Use case

I wrote a monitoring recipe which installs monit and add some templates, here's a fragment of code

# Install and configure monit
include_recipe 'monit'
# Template shouldn't be installed if apache is not present
monitrc 'apache2' do
  template_cookbook 'monitoring_role'
  # My previous attempt, which doesn't work if recipe is included with include_recipe
  only_if { node.recipes.include?('apache2') || node[:monit][:templates].include?('apache2') }
end

I've added an attribute (node[:monit][:templates]) to manually specify monit templates in node attributes, but it's pretty easy to forget them. Obviously I don't want the apache2 recipe included in all hosts but I'm using this recipe in multiple machines and with the check I'm asking for I can reuse the recipe on all hosts and monit will be installed with templates for daemons present in the current node.

Foi útil?

Solução

Sure, you can inspect the run_context. This is how ChefSpec does it:

#
# Automatically appends "+::default+" to recipes that need them.
#
# @param [String] name
#
# @return [String]
#
def with_default(name)
  name.include?('::') ? name : "#{name}::default"
end

#
# The list of loaded recipes on the Chef run (normalized)
#
# @return [Array<String>]
#
def loaded_recipes
  @runner.run_context.loaded_recipes.map { |name| with_default(name) }
end

So that answers your question. Now my question - why?

Chef will automatically build up the dependency graph for you. It will only include the recipe once, at the appropriate place of order. You don't need to manually manage this graph, and it is "okay" to include the same recipe more than once.

In fact, internally, we use this pattern quite frequently. Each recipe declares the recipes it depends on at the very top. I'd recommend watching this video: https://www.youtube.com/watch?v=hYt0E84kYUI

Outras dicas

Final toughts

sethvargo answer is correct, I can add a ruby file in the library folder of my cookbook with the following code to get the method I'm asking for

class Chef

  class Node
    def has_recipe?(recipe_name)
      loaded_recipes.include?(with_default(recipe_name))
    end

    private
    #
    # Automatically appends "+::default+" to recipes that need them.
    #
    # @param [String] name
    #
    # @return [String]
    #
    def with_default(name)
      name.include?('::') ? name : "#{name}::default"
    end

    #
    # The list of loaded recipes on the Chef run (normalized)
    #
    # @return [Array<String>]
    #
    def loaded_recipes
      node.run_context.loaded_recipes.map { |name| with_default(name) }
    end

  end

end

The example in my use case above is broken since monitrc is a definition and thus doesn't accept only_if/not_if blocks. So I need to rewrite that piece of code using plain ruby which is evaluated at recipe compile time (see this). This is the modified code

if node.has_recipe?('apache2') # This if is evaluated when recipes are loaded
  monitrc 'apache2' do
    template_cookbook 'monitoring_role'  
  end
end

The important thing is that this code can have an inconsistent behavior. It depends on when the checked recipe is included. If I call this recipe monitoring::default and I have a recipe named lamp::default which calls include_recipe 'apache2', in the following case the template is installed

run_list = ['lamp::default', 'monitoring::default']

while with this run list the call is evaluated to false

run_list = ['monitoring::default', 'lamp::default']

The has_recipe? method works fine with execute time guards, i.e.

package('foo') do
  only_if { node.has_recipe?('bar') }
end

works in every case.

There is also a recipe? available method in chef node, to check if a recipe is included in node run_list

Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top