Code Sharing in Platform Specific 1-1 Custom Resources
One of the most complicated uses of HWRP-style resources in Chef is to do platform-specific providers. The old model of doing this looks something like the service resource and providers, with a wrapping single resource with many providers behind it.
This 1:N model of resources and providers does not work with Custom Resources by design (one of the two limitations
of using custom resources). The model that is preferred, though, is the 1:1 model used by the package resources
where when users type package
in the DSL they will get different resources and providers based on the platform
they are running on. On Ubuntu/Debian systems package
is wired up to the apt_package
resource and provider,
while on RHEL-based systems package
is wired up to the yum_package
resource and provider.
In core chef (at the time this is written) all the package resources and provider share a base class which they inherit from. The other limitation of custom resources is that you cannot use inheritance (the only limitation of the custom resource DSL is that you do not create the class, can not control the class name, and can not control what base class it inherits from). That means that authors must use modules for code-sharing.
A full example of this model with custom resources looks like this:
recipes/default.rb:
doit "whatever" do
mynamearg "whatever2"
end
cat resources/doit_debian.rb:
provides :doit_debian
resource_name :doit_debian
provides :doit, platform_family: "debian"
include ResourceDoitBase
action :run do
file "/tmp/doit.debian" do
content "debian"
end
action_helper
end
cat resources/doit_debian.rb:
provides :doit_debian
resource_name :doit_debian
provides :doit, platform_family: "debian"
include ResourceDoitBase
action :run do
file "/tmp/doit.debian" do
content "debian"
end
action_helper
end
cat libraries/resource_doit_base.rb:
module ResourceDoitBase
def self.included(base)
base.class_eval do
#
# Start of the shared code
#
property :mynamearg, String, name_property: true
action_class do
def action_helper
puts "HELPED!"
end
end
#
# End of the shared code
#
end
end
end
spec/unit/recipes/default_spec.rb:
require 'spec_helper'
describe 'platform_resources::default' do
context 'When all attributes are default, on an Ubuntu 16.04' do
let(:chef_run) do
runner = ChefSpec::ServerRunner.new(platform: 'ubuntu', version: '16.04', step_into: ['doit'])
runner.converge(described_recipe)
end
it 'converges successfully' do
expect { chef_run }.to_not raise_error
end
it 'creates the /tmp/doit.debian file with the right contents' do
expect(chef_run).to render_file('/tmp/doit.debian').with_content('debian')
end
end
context 'When all attributes are default, on an CentOS 7.3.1611' do
let(:chef_run) do
# for a complete list of available platforms and versions see:
# https://github.com/customink/fauxhai/blob/master/PLATFORMS.md
runner = ChefSpec::ServerRunner.new(platform: 'centos', version: '7.3.1611', step_into: ['doit'])
runner.converge(described_recipe)
end
it 'converges successfully' do
expect { chef_run }.to_not raise_error
end
it 'creates the /tmp/doit.rhel file with the right contents' do
expect(chef_run).to render_file('/tmp/doit.rhel').with_content('rhel')
end
end
end
The provides
lines are responsible for correctly wiring up the platform-specific resources. Each one wires up a
name for the custom resource which is unversal on any platform doit_debian
and doit_rhel
. The resource_name
is
set to the universal name so that in chef-client output it can be easily determined which resource actually
ran (similar to how in chef >= 12.0 package "foo"
will show as yum_package[foo]
on RHEL and apt_package[foo]
on
Ubuntu). Then there is the platform-specific provide mapping for doit
. On platforms other than debian or
RHEL platforms typing doit
will fail (the doit_rhel
provider might need to consider the other platform families
of fedora
, amazon
and/or suse
).
The chefspec tests ensure that the platform-specific behavior is run correctly on different platforms.
The shared code is in the library file. If you try to insert property
lines directly into a module then it will fail
because at the time the module is being parsed the module does not know anything about properties or the action class.
Using the ruby included
hook with a class_eval
lazies all the statements contained in the class_eval
to when the
module is included into another class. The statements inside the class_eval
are run in the class just like if they
were being typed into the resources file.
The action_class
helper shows how to share code across actions across different custom resources. It is maximally
useless in this example since it just prints a string.
The syntax in the library file is raw ruby and perhaps we could make this more elegant at some point in the future (but since this just ruby we’re never going to break it and you can go ahead and use it).