Hooking in a custom provider for an entire chef run


is it possible to overwrite the default provider for a built-in resource (say service or package) for an entire chef run?

How would I do that?

Probably I missed existing answers to this question, my apologies if so.



It's technically possible, but highly brittle, a severe footgun and begs the question of why you want to do it instead of providing a PR upstream ?

Could you ellaborate on the reasons so we may better help/give advice on how to achieve that ?


The scenario is as follows:
We use a bunch of cookbooks to configure "normal" machines (say Linux desktops).

We also have nodes that mount their root fs from NFS read-only. Their setup is split in two parts:

  1. The setup of the OS image(s) on the NFS server(s) and
  2. the individual setup of the nodes with NFS root.

It would be nice to configure stand-alone and netboot nodes with the same cookbooks, but:
service { action :start } should be a no-op inside the OS images on the NFS server,
while package will fail on the read-only netboot nodes.

Therefore I was thinking about replacing the affected resources and actions with no-ops.

That's why the action :nothing exists.

You'll need to have a way to differenciate which machine the cookbook is running on, but that could be something like (using an attribute of nfs_server being true or false)

package 'xxx' do
  action node['nfs_server'] ? :install : :nothing

service 'xxx' do
  action node['nfs_server'] ? :nothing : :start

Recipes are evaluated as ruby code, so you can do logic within the recipe code based on attributes or something else.


If I don't want the provider to do :nothing but :something_else the functional equivalent would be:

package 'asdf' do
  provider MyCustomProvider if node['nfs_server']

In either case I would need to change dozens, maybe hundreds of resources, hence my question if this could be tweaked centrally.

But your suggestion is indeed very helpful as this may be a promising approach:

Chef::Resource::Package.default_action = %i[nothing] if node['nfs_server']

I'll look further into that direction.

Many thanks

I think we'd all need code to understand what you're willing to do here.

Conditional action on resource are pretty common, like for files or services we can use the ternary operator to say which action we want based on attribute as recipes are evaulated as pure ruby, like this:

file '/path/to/file' do
  content 'A content to update'
  action node['my_namespace']['do_not_overwrite'] ? :create_if_missing : :create

service 'my_service' so
  action node['my_namespace']['start_enable'] ? [:start, :enable] : :nothing

Or for something more complex based on distribution type (pseudo code, just to give an idea):

if node['platform'] == 'redhat'
   package 'httpd'
   service 'httpd' do
     action [:start, :enable]

  template '/etc/httpd/....' do
    source 'your_template.erb'
    action xx
    notifies :reload, 'service[httpd]', :immediately
  package 'apache2' 
  service 'apache2' do
     action [:start, :enable]

  template '/etc/apache2/conf.d/xxx.conf' do
    source "XXX.erb"
    notifies :restart, 'service[apache2]'

Hi Tensibai

I think get your point. My use case is to reuse existing cookbooks in this new scenario with as little modification as possible.

For now I found a working solution monkey-patching Chef::Resource.provider in a wrapper-cookbookslibraries/….rb.

This is my custom service provider:

# use systemd provider but turn :start and :restart into no-ops
class Chef::Provider::Service::Netboot < Chef::Provider::Service::Systemd
  def start_service
    Chef::Log.info("Stubbing systemctl start #{new_resource}")    

  def restart_service
    Chef::Log.info("Stubbing systemctl restart #{new_resource}")    

And this is the monkey patch:

class Chef::Resource::Service
  def provider(arg = nil)
    set_or_return(:provider, Chef::Provider::Service::Netboot, kind_of: [ Class ])

That's nowhere near an optimal solution and it surely breaks

service 'my-service' do
  provider Chef::Provider::Service::Foo

A better solution would be to inject my provider into the first place of Chef::ProviderResolver.prioritized_handlers (via Chef.set_provider_priority_array :service, …?) but that seems to be even uglier and I haven't managed to get this to work.


P.S.: In my use case the custom service provider is required anyhow as Chef does not properly detect systemd inside the server-side chroots but falls back to Chef::Provider::Service::Debian which is completely wrong.