Transcript
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


Top Related