Chef is awesome, but it’s also very easy to go overboard. In terms of testing and maintainability, sometimes its better to refactor your long recipe into an LWRP. As your infrastructure evolves, so should you cookbooks. But at some point your bound to have a cookbook 500+ lines of antiquated logic. How do you refactor such a large chunk of code that is critical to your infrastructure? How much logic should me moved into other cookbooks? How much logic should be extracted into LWRPs? How much logic should be moved out of Chef, into Ruby, and packaged as a gem?
12. # This file is managed by Chef for "<%= node['fqdn'] %>"
# Do NOT modify this file by hand.
<%= node['ipaddress'] %> <%= node['fqdn'] %>
127.0.0.1!localhost <%= node['fqdn'] %>
255.255.255.255!broadcasthost
::1 localhost
fe80::1%lo0! localhost
templates/default/etc/hosts.erb
15. # This file is managed by Chef for "<%= node['fqdn'] %>"
# Do NOT modify this file by hand.
<%= node['ipaddress'] %> <%= node['fqdn'] %>
127.0.0.1!localhost <%= node['fqdn'] %>
255.255.255.255!broadcasthost
::1 localhost
fe80::1%lo0! localhost
# Custom Entries
<% node['etc']['hosts'].each do |h| -%>
<%= h['ip'] %> <%= h['host'] %>
<% end -%>
templates/default/etc/hosts.erb
25. TODO:Add infographics
# This file is managed by Chef for "www.myapp.com"
# Do NOT modify this file by hand.
1.2.3.4 www.myapp.com
127.0.0.1!localhost www.myapp.com
255.255.255.255!broadcasthost
::1 localhost
fe80::1%lo0! localhost
# Custom Entries
1.2.3.4 www.example.com
4.5.6.7 foo.example.com
7.8.9.0 bar.example.com
/etc/hosts
26. TODO:Add infographics
# This file is managed by Chef for "www.myapp.com"
# Do NOT modify this file by hand.
1.2.3.4 www.myapp.com
127.0.0.1!localhost www.myapp.com
255.255.255.255!broadcasthost
::1 localhost
fe80::1%lo0! localhost
# Custom Entries
7.8.9.0 bar.example.com
/etc/hosts
43. # This file is managed by Chef for "<%= node['fqdn'] %>"
# Do NOT modify this file by hand.
<%= node['ipaddress'] %> <%= node['fqdn'] %>
127.0.0.1!localhost <%= node['fqdn'] %>
255.255.255.255!broadcasthost
::1 localhost
fe80::1%lo0! localhost
# Custom Entries
<%= @hosts.join("n") %>
templates/default/etc/hosts.erb
47. require 'chefspec'
describe 'hostsfile::default' do
let(:hosts) { ['1.2.3.4 example.com', '4.5.6.7 bar.com'] }
before do
Chef::Recipe.any_instance.stub(:data_bag).with('etc_hosts').and_return(hosts)
end
end
spec/default_spec.rb
48. require 'chefspec'
describe 'hostsfile::default' do
let(:hosts) { ['1.2.3.4 example.com', '4.5.6.7 bar.com'] }
before do
Chef::Recipe.any_instance.stub(:data_bag).with('etc_hosts').and_return(hosts)
end
let(:runner) { ChefSpec::ChefRunner.new.converge('hostsfile::default') }
end
spec/default_spec.rb
49. require 'chefspec'
describe 'hostsfile::default' do
let(:hosts) { ['1.2.3.4 example.com', '4.5.6.7 bar.com'] }
before do
Chef::Recipe.any_instance.stub(:data_bag).with('etc_hosts').and_return(hosts)
end
let(:runner) { ChefSpec::ChefRunner.new.converge('hostsfile::default') }
it 'loads the data bag' do
Chef::Recipe.any_instance.should_receive(:data_bag).with('etc_hosts')
end
end
spec/default_spec.rb
50. require 'chefspec'
describe 'hostsfile::default' do
let(:hosts) { ['1.2.3.4 example.com', '4.5.6.7 bar.com'] }
before do
Chef::Recipe.any_instance.stub(:data_bag).with('etc_hosts').and_return(hosts)
end
let(:runner) { ChefSpec::ChefRunner.new.converge('hostsfile::default') }
it 'loads the data bag' do
Chef::Recipe.any_instance.should_receive(:data_bag).with('etc_hosts')
end
it 'creates the /etc/hosts template' do
expect(runner).to create_template('/etc/hosts').with_content(hosts.join("n"))
end
end
spec/default_spec.rb
61. hosts = data_bag('etc_hosts')
hosts << search(:node, 'role:mongo_master').first.tap do |n|
"#{n['ip_address']} #{n['fqdn']}"
end
template '/etc/hosts' do
owner 'root'
group 'root'
source 'etc/hosts'
variables(
hosts: hosts
)
end
recipes/default.rb
62. hosts = data_bag('etc_hosts')
hosts << search(:node, 'role:mongo_master').first.tap do |n|
"#{n['ip_address']} #{n['fqdn']}"
end
hosts << search(:node, 'role:mysql_master').first.tap do |n|
"#{n['ip_address']} #{n['fqdn']}"
end
hosts << search(:node, 'role:redis_master').first.tap do |n|
"#{n['ip_address']} #{n['fqdn']}"
end
template '/etc/hosts' do
owner 'root'
group 'root'
recipes/default.rb
76. Test that the Provider implements
the proper Ruby classes
77. TODO:Add infographics
class Entry
attr_accessor :ip_address, :hostname, :aliases, :comment
def initialize(options = {})
if options[:ip_address].nil? || options[:hostname].nil?
raise ':ip_address and :hostname are both required options'
end
@ip_address = options[:ip_address]
@hostname = options[:hostname]
@aliases = [options[:aliases]].flatten
@comment = options[:comment]
end
# ...
end
libraries/entry.rb
78. TODO:Add infographics
class Manipulator
def initialize
contents = ::File.readlines(hostsfile_path)
@entries = contents.collect do |line|
Entry.parse(line) unless line.strip.nil? || line.strip.empty?
end.compact
end
def add(options = {})
@entries << Entry.new(
ip_address: options[:ip_address],
hostname: options[:hostname],
aliases: options[:aliases],
comment: options[:comment]
)
end
end
libraries/manipulator.rb
79. # Creates a new hosts file entry. If an entry already exists, it
# will be overwritten by this one.
action :create do
hostsfile.add(
ip_address: new_resource.ip_address,
hostname: new_resource.hostname,
aliases: new_resource.aliases,
comment: new_resource.comment
)
new_resource.updated_by_last_action(true) if hostsfile.save
end
providers/entry.rb
83. TODO:Add infographics
describe Entry do
describe '.initialize' do
subject { Entry.new(ip_address: '2.3.4.5', hostname:
'www.example.com', aliases: ['foo', 'bar'], comment: 'This is a
comment!', priority: 100) }
it 'raises an exception if :ip_address is missing' do
expect {
Entry.new(hostname: 'www.example.com')
}.to raise_error(ArgumentError)
end
it 'sets the ip_address' do
expect(subject.ip_address).to eq('2.3.4.5')
end
end
spec/entry_spec.rb
87. TODO:Add infographics
describe 'hostsfile lwrp' do
let(:manipulator) { double('manipulator') }
before do
Manipulator.stub(:new).and_return(manipulator)
Manipulator.should_receive(:new).with(kind_of(Chef::Node))
.and_return(manipulator)
manipulator.should_receive(:save!)
end
let(:chef_run) {
ChefSpec::ChefRunner.new(
cookbook_path: $cookbook_paths,
step_into: ['hostsfile_entry']
)
}
spec/default_spec.rb
88. TODO:Add infographics
context 'actions' do
describe ':create' do
it 'adds the entry' do
manipulator.should_receive(:add).with({
ip_address: '2.3.4.5',
hostname: 'www.example.com',
aliases: nil,
comment: nil,
priority: nil
})
chef_run.converge('fake::create')
end
end
end
end