Need help with notifies attribute in LWRP DSL

I’m trying to write a package resource wrapper cookbook, say company_package, that will overwrite the default retries and retry_count attributes for the package resource. Of course, for the cookbook to fully support the package cookbook, I have to define the other attributes.

For notifies, which accepts 3 different parameters, what would the Ruby type, that is, what would be its :kind_of syntax in the resource DSL? I’ve read this https://docs.chef.io/resource_package.html

Chris

I am not sure what you mean: If you want to define attributes like notfies, not_if, only_if etc. yourself I am not sure if that is possible at all but it for sure is not necessary. Look at the “common functionaly” in the chef docs. Those are things that are inherited from the base classes. Look for the old documentations of HWRP if you want to know a little more about custom resources and how they work internally.

Here’s how I define my attributes in the resource/default.rb file of my LWRP

attribute :notifies, :name_attribute => false, :kind_of => ???, :required => false

What should the “kind_of” be?

It needs to ultimately support this

# my_company package is an LWRP that wraps the package resource
mycompany_package 'some_package' do
   notifies :restart, 'service[some_package1]', :immediately
end

IOW it needs to be a “kind_of” that supports an array of Strings and Symbols. So should it be like this?

attribute :notifies, :name_attribute => false, :kind_of => [String, Symbol], :required => false

You should not include the base attributes such as notifies, action, etc. You get those “for free” from chef::Resource and LWRPBase derrives from that.

Thanks Matt, but that was NOT my experience. For example, I had the following in one of my recipes

mycompany_package 'java' do
  flush_cache [:before, :before]
end

and I got an error that flush_cache was not defined in mycompany_package resource? That is when I went down this rabbit hole defining all the attributes the package resource has. What am I missing?

Here’s what I really (just) need.

I want to override the retries and retry_count attributes of the package resource so I can just replace package resource in ALL my recipes with mycompany_package, which will use the MY retries and retry_count values. I know I can just modify my package resource calls in my recipes to include these two values, but I have a LOT of package resource calls. Make sense?

Matt is talking about the base functionality that all resources have, i.e., the stuff documented here: https://docs.chef.io/resource_common.html It includes notifications and not_if/only_if.

In your example, flush_cache is only relevant to a package (i.e., it would make no sense for, say, a file resource to have this property), which is why it is not in the base resource.

A note about the more general aspect of your question: the parameter validation in Chef doesn’t handle every case, it’s designed to handle simple cases easily, but for parameters that have several arguments or more complex types (e.g., a Hash with specific kinds of keys and values or a nested structure), you can’t express the validation constraints with the built-in parameter validation. You have to either omit parameter validation or roll your own.

Thanks. In my case, I was trying to wrap a package resource, where indeed a flush_cache attribute makes sense?

To a human it makes perfect sense, but ruby has no way to know that; it just sees that you have a class MyPackage that inherits from Chef::Resource (which inherits from Object) and none of those things define that method.

To do what you want, there's a few options:

  1. Add all the properties of package that your users want to set to your LWRP and then copy them down to the "real" package resource. This is a bit tedious, but it has a lot of benefits. Look up "composition over inheritance" if you want to read more about that.
  2. Use a DSL method instead of a LWRP to do the wrapping. This is probably trickier if you're new to ruby/chef, but in the end would be a pretty simple implementation. Something like this:
def mycorp_package(name, &block)
  r = package(name, &block)
  r.retries(5)
  r.retry_delay(600)
end

You would put that in a module in a library and then include that into Chef::DSL::Recipe
3. Do a more HWRP style and have your custom package class inherit from Chef::Resource::Package. This will get you all the package properties automatically, but getting the provider stuff wired up correctly could be a pain. Your code will also be tightly coupled to the parent class, which exposes you to additional breakage conditions compared to a composition technique.

HTH

Thanks you. I’ve been exploring your option 1, and yes the only way for this to make sense is if I include ALL the attributes of the Package resource in my resource/default.rb file. Hence I asked the question, what should this line look like?

attribute :notifies, :name_attribute => false, :kind_of => ???

I’m also now looking at your option 3 regarding doing a HWRP instead.

Maybe I’ll combine a LWRP + HWRP.

@kallistec

I’m now exploring your option #3 - HWRP. This is my first HWRP. I have fundamental questions.

I have a cookbook named mycorp_package. Hence I have a libraries/mycorp_package.rb file that may have

class Chef
  class Provider
    class Package
      class MyCorpPackage < Chef::Provider::Package
        def load_current_reource
         ................
        end
        
        def install_package
        ..............
        end

        def remove_package
        ..............
        end
      end
    end
  end
end

Questions:

  • Since I’m extending the package resource, I don’t need a corresponding libraries/some_resource.rb file?

  • How do I now call this HWRP in another cookbook? I tried the following but I get a compile error

    mycorp_package ‘some_package’ end
    action :install
    end

    NoMethodError

    No resource or method named mycorp_package' forChef::Recipe “default”’

Hello

The resource describes what the desired state of the object (such as file, directory, package).
The provider is the bit of code that will carry out the work to make the object look likes it’s desired state.

In your recipe you are describing resources, these resources are actually Ruby classes that store the state. You have written a provider to carry out your actions to get to your desired state but have you coded a mycorp_package resource to store what the state should be?

As a first shot I would write a LWRP not a HWRP but it’s whatever you feel comfortable with.

@Matt_Wrock

Again, I need my LWRP to support this

mycompany_package 'package_name' do
  action :install
  notifies :restart, 'service[some_service]', :immediately
end

If I understand correctly, I need to create the ‘action’ and ‘notifies’ attributes for my resource, correct?

How do I get my LWRP to work the way I want if I don’t set a “notifies” attribute? I’ll need to do this in my resource/default.rb file…

attribute :notifies, :name_attribute => false, :kind_of => ???, :required => false

So I can do the following in my provider/default.rb, correct?

action :install do
  package new_resource.name do
    notifies new_resource.notifies
  end
end

You should not have to implement :notifies. I know that might seem a bit odd and you are wondering why it would “jut works”. The reason is that when you create an LWRP, chef creates a class that derives from LWRPBase and that derrives from Chef::Resource which provides the notifies method (see https://github.com/chef/chef/blob/master/lib/chef/resource.rb#L243).

That said, I’m guessing that the reason you are asking this is that you have already tried this and notifies did not work. Is that a correct guess? If so, what did happen? Anything? Was there an exception or did notifies simply not fire?

I get the following when it gets to the notifies call inside my action :Install block in my provider/default.rb file

 ArgumentError
    -------------
    wrong number of arguments (0 for 2..3)

would you be able to share your code repo link or a gist to your action :Install?

It’s simply this, in my provider/deffault.rb file

action :install do
  package new_resource.name do
    action :install
    retries new_resource.retries
    retry_delay new_resource.retry_delay
    notifies new_resource.notifies
  end
end

To work with this, in my resource/default.rb file

# Package name
attribute :name, String, required: true, name_property: true
# Override the retires and retry_delay attributes, which is the whole purpose of wrapping
# the package resource into mycompany_package LWRP
# Retries
attribute :retries, Integer, required: false, default: 3
# Delay between retries
attribute :retry_delay, Integer, required: false, default: 5

You don’t need to proxy notifies like that. It is handled for you already.

How does this

mycompany_package 'package_name' do
  action :install
  notifies :restart, 'service[some_service]', :immediately
end

Ultimately end up to be this?

action :install do
  package new_resource.name do
    action :install
    retries new_resource.retries
    retry_delay new_resource.retry_delay
    notifies :restart, 'service[some_service]', :immediately
  end
end

I’m (and I know it’s me) missing that connection!

This has to work too.

    mycompany_package 'package_name' do
      action :install
      notifies :start, 'service[some_service]'
    end

It doesn’t per se. All sub-resources within the action method get checked for their updated? flag when the action completes, and if it any are set, it sets the flag on the custom resource. That in turn triggers notifications declared on the custom resource instance. It’s a more generic form of what you describe.