109
Cookbook Refactoring A

Cookbook refactoring & abstracting logic to Ruby(gems)

Embed Size (px)

DESCRIPTION

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?

Citation preview

Page 1: Cookbook refactoring & abstracting logic to Ruby(gems)

Cookbook Refactoring

A

Page 2: Cookbook refactoring & abstracting logic to Ruby(gems)

Cookbook Refactoring

... and extracting logic into Rubygems

A

Page 3: Cookbook refactoring & abstracting logic to Ruby(gems)

[email protected]

E

byz

Page 4: Cookbook refactoring & abstracting logic to Ruby(gems)
Page 5: Cookbook refactoring & abstracting logic to Ruby(gems)

We're Hiring!

Page 6: Cookbook refactoring & abstracting logic to Ruby(gems)

We're Hiring!

Colorado

Page 7: Cookbook refactoring & abstracting logic to Ruby(gems)

New Branding

We're Hiring!

Page 8: Cookbook refactoring & abstracting logic to Ruby(gems)

UDO YOU SOMETIMES

FEEL LIKE

THIS

Page 9: Cookbook refactoring & abstracting logic to Ruby(gems)
Page 10: Cookbook refactoring & abstracting logic to Ruby(gems)
Page 11: Cookbook refactoring & abstracting logic to Ruby(gems)

template '/etc/hosts' do owner 'root' group 'root' source 'etc/hosts'end

recipes/default.rb

Page 12: Cookbook refactoring & abstracting logic to Ruby(gems)

# 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

Page 13: Cookbook refactoring & abstracting logic to Ruby(gems)
Page 14: Cookbook refactoring & abstracting logic to Ruby(gems)

default['etc']['hosts'] = [] unless node['etc']['hosts']

attributes/default.rb

Page 15: Cookbook refactoring & abstracting logic to Ruby(gems)

# 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

Page 16: Cookbook refactoring & abstracting logic to Ruby(gems)

include_attribute 'hostsfile'

default['etc']['hosts'] << { 'ip' => '1.2.3.4', 'host' => 'www.example.com'}

other_cookbook/attributes/default.rb

Page 17: Cookbook refactoring & abstracting logic to Ruby(gems)

node.default['etc']['hosts'] << { 'ip' => '1.2.3.4', 'host' => 'www.example.com'}

other_cookbook/recipes/default.rb

Page 18: Cookbook refactoring & abstracting logic to Ruby(gems)

default_attributes({ 'etc' => { 'hosts' => [ {'ip' => '1.2.3.4', 'host' => 'www.example.com'}, {'ip' => '4.5.6.7', 'host' => 'foo.example.com'} ] }})

roles/my_role.rb

Page 19: Cookbook refactoring & abstracting logic to Ruby(gems)

{ "default_attributes": { "etc": { "hosts": [ {"ip": "1.2.3.4", "host": "www.example.com"}, {"ip": "4.5.6.7", "host": "foo.example.com"} ] } }}

environments/production.json

Page 20: Cookbook refactoring & abstracting logic to Ruby(gems)
Page 21: Cookbook refactoring & abstracting logic to Ruby(gems)

node.set['etc']['hosts'] = { ip: '7.8.9.0', host: 'bar.example.com'})

recipes/default.rb

Page 22: Cookbook refactoring & abstracting logic to Ruby(gems)
Page 23: Cookbook refactoring & abstracting logic to Ruby(gems)

arr = [1,2,3]

arr << 4 => [1,2,3,4]arr = 4 => 4

Page 24: Cookbook refactoring & abstracting logic to Ruby(gems)

arr = [1,2,3]

arr << 4 => [1,2,3,4]arr = 4 => 4

Not an Array

Page 25: Cookbook refactoring & abstracting logic to Ruby(gems)

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.com127.0.0.1!localhost www.myapp.com255.255.255.255!broadcasthost::1 localhost fe80::1%lo0! localhost

# Custom Entries1.2.3.4 www.example.com4.5.6.7 foo.example.com7.8.9.0 bar.example.com

/etc/hosts

Page 26: Cookbook refactoring & abstracting logic to Ruby(gems)

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.com127.0.0.1!localhost www.myapp.com255.255.255.255!broadcasthost::1 localhost fe80::1%lo0! localhost

# Custom Entries7.8.9.0 bar.example.com

/etc/hosts

Page 27: Cookbook refactoring & abstracting logic to Ruby(gems)

Post Mortem

Page 28: Cookbook refactoring & abstracting logic to Ruby(gems)
Page 29: Cookbook refactoring & abstracting logic to Ruby(gems)
Page 30: Cookbook refactoring & abstracting logic to Ruby(gems)

<< =

Page 31: Cookbook refactoring & abstracting logic to Ruby(gems)

<< =!=

Page 32: Cookbook refactoring & abstracting logic to Ruby(gems)

Post Mortem

Action Items

7

Page 33: Cookbook refactoring & abstracting logic to Ruby(gems)

Monkey patch Chef to raise an exception when redefining that

particular node attribute.

Page 34: Cookbook refactoring & abstracting logic to Ruby(gems)

Monkey patch Chef to raise an exception when redefining that

particular node attribute.t

Page 35: Cookbook refactoring & abstracting logic to Ruby(gems)

Create a special cookbook that uses a threshold value and raises an

exception if the size of the array doesn't "make sense".

Page 36: Cookbook refactoring & abstracting logic to Ruby(gems)

Create a special cookbook that uses a threshold value and raises an

exception if the size of the array doesn't "make sense".t

Page 37: Cookbook refactoring & abstracting logic to Ruby(gems)

Move all entries to a data bag

Page 38: Cookbook refactoring & abstracting logic to Ruby(gems)

Move all entries to a data bag

u

Page 39: Cookbook refactoring & abstracting logic to Ruby(gems)

Move all entries to a data bag66 Add tests

Page 40: Cookbook refactoring & abstracting logic to Ruby(gems)

Data Bags

Page 41: Cookbook refactoring & abstracting logic to Ruby(gems)

[ "1.2.3.4 example.com www.example.com", "4.5.6.7 foo.example.com", "7.8.9.0 bar.example.com"]

data_bags/etc_hosts.json

Page 42: Cookbook refactoring & abstracting logic to Ruby(gems)

hosts = data_bag('etc_hosts')

template '/etc/hosts' do owner 'root' group 'root' source 'etc/hosts' variables( hosts: hosts )end

recipes/default.rb

Page 43: Cookbook refactoring & abstracting logic to Ruby(gems)

# 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

Page 44: Cookbook refactoring & abstracting logic to Ruby(gems)

Move all entries to a data bag56 Add tests

Page 45: Cookbook refactoring & abstracting logic to Ruby(gems)

require 'chefspec'

spec/default_spec.rb

Page 46: Cookbook refactoring & abstracting logic to Ruby(gems)

require 'chefspec'

describe 'hostsfile::default' do

end

spec/default_spec.rb

Page 47: Cookbook refactoring & abstracting logic to Ruby(gems)

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

Page 48: Cookbook refactoring & abstracting logic to Ruby(gems)

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

Page 49: Cookbook refactoring & abstracting logic to Ruby(gems)

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

Page 50: Cookbook refactoring & abstracting logic to Ruby(gems)

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")) endend

spec/default_spec.rb

Page 51: Cookbook refactoring & abstracting logic to Ruby(gems)

$ rspec cookbooks/hostsfile

Running all specs

Page 52: Cookbook refactoring & abstracting logic to Ruby(gems)

$ rspec cookbooks/hostsfile

Running all specs

**

Finished in 0.0003 seconds2 examples, 0 failures

Page 53: Cookbook refactoring & abstracting logic to Ruby(gems)

$ rspec cookbooks/hostsfile

Running all specs

**

Finished in 0.0003 seconds2 examples, 0 failures

Really Fucking Fast™

Page 54: Cookbook refactoring & abstracting logic to Ruby(gems)

#winning

Page 55: Cookbook refactoring & abstracting logic to Ruby(gems)

10,000 tests

Page 56: Cookbook refactoring & abstracting logic to Ruby(gems)

28 seconds

Page 57: Cookbook refactoring & abstracting logic to Ruby(gems)

#winning

Page 58: Cookbook refactoring & abstracting logic to Ruby(gems)

⏳⏳

Page 59: Cookbook refactoring & abstracting logic to Ruby(gems)
Page 60: Cookbook refactoring & abstracting logic to Ruby(gems)
Page 61: Cookbook refactoring & abstracting logic to Ruby(gems)

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

Page 62: Cookbook refactoring & abstracting logic to Ruby(gems)

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' source 'etc/hosts' variables( hosts: hosts )end

recipes/default.rb

Page 63: Cookbook refactoring & abstracting logic to Ruby(gems)

LWRPs

Page 64: Cookbook refactoring & abstracting logic to Ruby(gems)

# List of all actions supported by the provideractions :create, :create_if_missing, :update, :remove

# Make create the default actiondefault_action :create

# Required attributesattribute :ip_address, kind_of: String, name_attribute: true, required: trueattribute :hostname, kind_of: String

# Optional attributesattribute :aliases, kind_of: Arrayattribute :comment, kind_of: String

resources/entry.rb

Page 65: Cookbook refactoring & abstracting logic to Ruby(gems)

action :create do ::Chef::Util::FileEdit.search_file_delete_line(entry) ::Chef::Util::FileEdit.insert_line_after_match(/\n/, entry)end

protected

def entry [new_resource.ip_address, new_resource.hostname, new_resource.aliases.join(' ')].compact.join(' ').squeeze(' ') end

providers/entry.rb

Page 66: Cookbook refactoring & abstracting logic to Ruby(gems)

hostsfile_entry '1.2.3.4' do hostname 'example.com'end

providers/entry.rb

Page 67: Cookbook refactoring & abstracting logic to Ruby(gems)

Chef::Util::FileEdit is slow

Page 68: Cookbook refactoring & abstracting logic to Ruby(gems)

Re-writing the file on each run

Page 69: Cookbook refactoring & abstracting logic to Ruby(gems)

Provider kept growning

Page 70: Cookbook refactoring & abstracting logic to Ruby(gems)

Untested

Page 71: Cookbook refactoring & abstracting logic to Ruby(gems)

RefactorA

Page 72: Cookbook refactoring & abstracting logic to Ruby(gems)

Move to pure Ruby classes

Page 73: Cookbook refactoring & abstracting logic to Ruby(gems)

Ditch Chef::Util::FileEdit and manage the entire file

Page 74: Cookbook refactoring & abstracting logic to Ruby(gems)

Only implement Ruby classes in the Provider (logic-less Provider)

Page 75: Cookbook refactoring & abstracting logic to Ruby(gems)

Test the Ruby code

Page 76: Cookbook refactoring & abstracting logic to Ruby(gems)

Test that the Provider implements the proper Ruby classes

Page 77: Cookbook refactoring & abstracting logic to Ruby(gems)

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

Page 78: Cookbook refactoring & abstracting logic to Ruby(gems)

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] ) endend

libraries/manipulator.rb

Page 79: Cookbook refactoring & abstracting logic to Ruby(gems)

# 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.saveend

providers/entry.rb

Page 80: Cookbook refactoring & abstracting logic to Ruby(gems)
Page 81: Cookbook refactoring & abstracting logic to Ruby(gems)
Page 82: Cookbook refactoring & abstracting logic to Ruby(gems)

RSpec

Page 83: Cookbook refactoring & abstracting logic to Ruby(gems)

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') endend

spec/entry_spec.rb

Page 84: Cookbook refactoring & abstracting logic to Ruby(gems)
Page 85: Cookbook refactoring & abstracting logic to Ruby(gems)

Chef Spec

Page 86: Cookbook refactoring & abstracting logic to Ruby(gems)

Chef Spec

Page 87: Cookbook refactoring & abstracting logic to Ruby(gems)

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

Page 88: Cookbook refactoring & abstracting logic to Ruby(gems)

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 endend

Page 89: Cookbook refactoring & abstracting logic to Ruby(gems)

Open It

Page 90: Cookbook refactoring & abstracting logic to Ruby(gems)
Page 91: Cookbook refactoring & abstracting logic to Ruby(gems)
Page 92: Cookbook refactoring & abstracting logic to Ruby(gems)
Page 93: Cookbook refactoring & abstracting logic to Ruby(gems)
Page 94: Cookbook refactoring & abstracting logic to Ruby(gems)
Page 95: Cookbook refactoring & abstracting logic to Ruby(gems)

Gem It

Page 96: Cookbook refactoring & abstracting logic to Ruby(gems)

$ bundle gem hostsfile

Page 97: Cookbook refactoring & abstracting logic to Ruby(gems)

$ bundle gem hostsfile create hostsfile/Gemfile create hostsfile/Rakefile create hostsfile/LICENSE.txt create hostsfile/README.md create hostsfile/.gitignore create hostsfile/hostsfile.gemspec create hostsfile/lib/hostsfile.rb create hostsfile/lib/hostsfile/version.rbInitializating git repo in ~Development/hostsfile

Page 98: Cookbook refactoring & abstracting logic to Ruby(gems)

entry.rb

manipulator.rb

99

Page 99: Cookbook refactoring & abstracting logic to Ruby(gems)

9

Page 100: Cookbook refactoring & abstracting logic to Ruby(gems)

9?

Page 101: Cookbook refactoring & abstracting logic to Ruby(gems)

chef_gem 'hostsfile'

recipes/default.rb

Page 102: Cookbook refactoring & abstracting logic to Ruby(gems)

require 'hostsfile'

providers/entry.rb

Page 103: Cookbook refactoring & abstracting logic to Ruby(gems)

In another cookbook...

Page 104: Cookbook refactoring & abstracting logic to Ruby(gems)

# ...

depends 'hostsfile'

other_cookbook/metadata.rb

Page 105: Cookbook refactoring & abstracting logic to Ruby(gems)

{ "run_list": [ "recipe[hostsfile]" ]}

www.myapp.com (Chef Node)

Page 106: Cookbook refactoring & abstracting logic to Ruby(gems)
Page 107: Cookbook refactoring & abstracting logic to Ruby(gems)
Page 108: Cookbook refactoring & abstracting logic to Ruby(gems)
Page 109: Cookbook refactoring & abstracting logic to Ruby(gems)

ThankYou

z