DRY within a Chef recipe
Asked Answered
T

2

17

What's the best way to do a little DRY within a chef recipe? I.e. just break out little bits of the Ruby code, so I'm not copying pasting it over and over again.

The following fails of course, with:

NoMethodError: undefined method `connect_root' for Chef::Resource::RubyBlock

I may have multiple ruby_blocks in one recipe, as they do different things and need to have different not_if blocks to be truley idempotent.

def connect_root(root_password)
  m = Mysql.new("localhost", "root", root_password)
  begin
    yield m
  ensure
    m.close
  end
end

ruby_block "set readonly" do
  block do
    connect_root node[:mysql][:server_root_password] do |connection|
      command = 'SET GLOBAL read_only = ON'
      Chef::Log.info "#{command}"
      connection.query(command)
    end
  end
  not_if do
    ro = nil
    connect_root node[:mysql][:server_root_password] do |connection|
      connection.query("SELECT @@read_only as ro") {|r| r.each_hash {|h| 
        ro = h['ro']
      } }
    end
    ro
  end
end
Tamis answered 24/3, 2013 at 4:25 Comment(0)
M
25

As you already figured out, you cannot define functions in recipes. For that libraries are provided. You should create a file (e.g. mysql_helper.rb ) inside libraries folder in your cookbook with the following:

module MysqlHelper
  def self.connect_root( root_password )
    m = Mysql.new("localhost", "root", root_password)
    begin
      yield m
    ensure
      m.close
    end
  end
end

It must be a module, not a class. Notice also we define it as static (using self.method_name). Then you will be able to use functions defined in this module in your recipes using module name with method name:

MysqlHelper.connect_root node[:mysql][:server_root_password] do |connection|
  [...]
end
Montford answered 27/3, 2013 at 20:38 Comment(5)
I didn't try it, but its a complete answer, with example. Thanks! I'm sad to see that I have to create a whole chef cookbook library for one little function that won't be used anywhere else. But whatever.Tamis
If you want to use instance methods so you don't have to prefix it with the class name, you would just do ::Chef::Recipe.send(:include, MysqlHelper) and then you could just call connect_root directly.Heartstricken
Thank you @JohnMorales. The accepted answer was perfect, however the line you added (shown below) got it working to completion: ::Chef::Recipe.send(:include, MysqlHelper)Spermatozoon
If you want to use module instance methods in recipes you can just add the line extend MysqlHelper in your recipe and then all the instance methods of MysqlHelper are available in this recipe. Calling include will instead add those methods to every recipe, which is sometimes too broad.Montford
I am using ChefDK v0.16.28 and am able to define methods inside of recipes. Seems like this may have changed in the last few years.Albrecht
T
1

For the record, I just created a library with the following. But that seems overkill for DRY within one file. I also couldn't figure out how to get any other namespace for the module to use, to work.

class Chef
  class Resource
    def connect_root(root_password)
      ...
Tamis answered 24/3, 2013 at 5:2 Comment(2)
The documentation for libraries in chef cookbooks is woefully inadequate and heavily ambiguous.Tamis
You should not monkeypatch the Resource class. It is much better to create a module and address methods through the module.Montford

© 2022 - 2024 — McMap. All rights reserved.