Custom Resources

[edit on GitHub]

A custom resource:

  • Is a simple extension of Chef Infra Client that adds your own resources
  • Is implemented and shipped as part of a cookbook
  • Follows easy, repeatable syntax patterns
  • Effectively leverages resources that are built into Chef Infra Client and/or custom Ruby code
  • Is reusable in the same way as resources that are built into Chef Infra Client

For example, Chef Infra Client includes built-in resources to manage files, packages, templates, and services, but it does not include a resource that manages websites.

Syntax

A custom resource is defined as a Ruby file and is located in a cookbook’s /resources directory. This file:

  • Declares the properties of the custom resource
  • Loads current state of properties, if the resource already exists
  • Defines each action the custom resource may take

The syntax for a custom resource is. For example:

property :property_name, RubyType, default: 'value'

action :action_name do
 # a mix of built-in Chef resources and Ruby
end

action :another_action_name do
 # a mix of built-in Chef resources and Ruby
end

where the first action listed is the default action.

Warning

Do not use existing keywords from the Chef Infra Client resource system in a custom resource, like “name”. For example, property :property_name in the following invalid syntax: property :name, String, default: 'thename'.

Example

This example site utilizes Chef’s built-in file, service and package resources, and includes :create and :delete actions. Since it uses built-in Chef Infra Client resources, besides defining the property and actions, the code is very similar to that of a recipe.

property :homepage, String, default: '<h1>Hello world!</h1>'

action :create do
  package 'httpd'

  service 'httpd' do
    action [:enable, :start]
  end

  file '/var/www/html/index.html' do
    content new_resource.homepage
  end
end

action :delete do
  package 'httpd' do
    action :remove
  end
end

where

  • homepage is a property that sets the default HTML for the index.html file with a default value of '<h1>Hello world!</h1>'
  • the action block uses the built-in collection of resources to tell Chef Infra Client how to install Apache, start the service, and then create the contents of the file located at /var/www/html/index.html
  • action :create is the default resource, because it is listed first; action :delete must be called specifically (because it is not the default action)

Once written, the custom resource may be used in a recipe just like any of the resources that are built into Chef Infra Client. The resource gets its name from the cookbook and from the file name in the /resources directory, with an underscore (_) separating them. For example, a cookbook named exampleco with a custom resource named site.rb is used in a recipe like this:

exampleco_site 'httpd' do
  homepage '<h1>Welcome to the Example Co. website!</h1>'
end

and to delete the exampleco website, do the following:

exampleco_site 'httpd' do
  action :delete
end

Scenario: website Resource

Create a resource that configures Apache httpd for Red Hat Enterprise Linux 7 and CentOS 7.

This scenario covers the following:

  1. Defining a cookbook named website
  2. Defining two properties
  3. Defining an action
  4. For the action, defining the steps to configure the system using resources that are built into Chef Infra
  5. Creating two templates that support the custom resource
  6. Adding the resource to a recipe

Create a Cookbook

This article assumes that a cookbook directory named website exists in a chef-repo with (at least) the following directories:

/website
  /recipes
  /resources
  /templates

You may use a cookbook that already exists or you may create a new cookbook.

See /ctl_chef.html for more information about how to use the chef command-line tool that is packaged with Chef Workstation to build the chef-repo, plus related cookbook sub-directories.

Objectives

Define a custom resource!

A custom resource typically contains:

  • A list of defined custom properties (property values are specified in recipes)
  • At least one action (actions tell Chef Infra Client what to do)
  • For each action, use a collection of resources that are built into Chef Infra Client to define the steps required to complete the action

What is needed?

This custom resource requires:

  • Two template files
  • Two properties
  • An action that defines all of the steps necessary to create the website

Define Properties

Custom properties are defined in the resource. This custom resource needs two:

  • instance_name
  • port

These properties are defined as variables in the httpd.conf.erb file. A template block in recipes will tell Chef Infra Client how to apply these variables.

In the custom resource, add the following custom properties:

property :instance_name, String, name_property: true
property :port, Integer, required: true

where

  • String and Integer are Ruby types (all custom properties must have an assigned Ruby type)
  • name_property: true allows the value for this property to be equal to the 'name' of the resource block

The instance_name property is then used within the custom resource in many locations, including defining paths to configuration files, services, and virtual hosts.

Define Actions

Each custom resource must have at least one action that is defined within an action block:

action :create do
  # the steps that define the action
end

where :create is a value that may be assigned to the action property for when this resource is used in a recipe.

For example, the action appears as a property when this custom resource is used in a recipe:

custom_resource 'name' do
  # some properties
  action :create
end

Define Resource

Use the package, template (two times), directory, and service resources to define the website resource. Remember: order matters!

package

Use the package resource to install httpd:

package 'httpd' do
  action :install
end

template, httpd.service

Use the template resource to create an httpd.service on the node based on the httpd.service.erb template located in the cookbook:

template "/lib/systemd/system/httpd-#{new_resource.instance_name}.service" do
  source 'httpd.service.erb'
  variables(
    instance_name: new_resource.instance_name
  )
  action :create
end

where

  • source gets the httpd.service.erb template from this cookbook
  • variables assigns the instance_name property to a variable in the template

template, httpd.conf

Use the template resource to configure httpd on the node based on the httpd.conf.erb template located in the cookbook:

template "/etc/httpd/conf/httpd-#{new_resource.instance_name}.conf" do
  source 'httpd.conf.erb'
  variables(
    instance_name: new_resource.instance_name,
    port: new_resource.port
  )
  action :create
end

where

  • source gets the httpd.conf.erb template from this cookbook
  • variables assigns the instance_name and port properties to variables in the template

Note

When writing a shared custom resource, you may need to use templates that ship with the custom resource. However, you will need to specify the cookbook containing the template by using the cookbook property in the template resource. If this is not set, then Chef Infra Client will look for templates in the location of the cookbook that is using the resource and won’t be able to find them. Example: cookbook 'website'

directory

Use the directory resource to create the /var/www/vhosts directory on the node:

directory "/var/www/vhosts/#{new_resource.instance_name}" do
  recursive true
  action :create
end

service

Use the service resource to enable, and then start the service:

service "httpd-#{new_resource.instance_name}" do
  action [:enable, :start]
end

Create Templates

The /templates directory must contain two templates:

  • httpd.conf.erb to configure Apache httpd
  • httpd.service.erb to tell systemd how to start and stop the website

httpd.conf.erb

httpd.conf.erb stores information about the website and is typically located under the /etc/httpd:

ServerRoot "/etc/httpd"
Listen <%= @port %>
Include conf.modules.d/*.conf
User apache
Group apache
<Directory />
  AllowOverride none
  Require all denied
</Directory>
DocumentRoot "/var/www/vhosts/<%= @instance_name %>"
<IfModule mime_module>
  TypesConfig /etc/mime.types
</IfModule>

Copy it as shown, add it under /templates, and then name the file httpd.conf.erb.

Template Variables

The httpd.conf.erb template has two variables:

  • <%= @instance_name %>
  • <%= @port %>

They are:

  • Declared as properties of the custom resource
  • Defined as variables in a template resource block within the custom resource
  • Tunable from a recipe when using port and instance_name as properties in that recipe
  • instance_name defaults to the 'name' of the custom resource if not specified as a property

httpd.service.erb

httpd.service.erb tells systemd how to start and stop the website:

[Unit]
Description=The Apache HTTP Server - instance <%= @instance_name %>
After=network.target remote-fs.target nss-lookup.target

[Service]
Type=notify

ExecStart=/usr/sbin/httpd -f /etc/httpd/conf/httpd-<%= @instance_name %>.conf -DFOREGROUND
ExecReload=/usr/sbin/httpd -f /etc/httpd/conf/httpd-<%= @instance_name %>.conf -k graceful
ExecStop=/bin/kill -WINCH ${MAINPID}

KillSignal=SIGCONT
PrivateTmp=true

[Install]
WantedBy=multi-user.target

Copy it as shown, add it under /templates, and then name it httpd.service.erb.

Final Resource

property :instance_name, String, name_property: true
property :port, Integer, required: true

action :create do
  package 'httpd' do
    action :install
  end

  template "/lib/systemd/system/httpd-#{new_resource.instance_name}.service" do
    source 'httpd.service.erb'
    variables(
      instance_name: new_resource.instance_name
    )
    action :create
  end

  template "/etc/httpd/conf/httpd-#{new_resource.instance_name}.conf" do
    source 'httpd.conf.erb'
    variables(
      instance_name: new_resource.instance_name,
      port: new_resource.port
    )
    action :create
  end

  directory "/var/www/vhosts/#{new_resource.instance_name}" do
    recursive true
    action :create
  end

  service "httpd-#{new_resource.instance_name}" do
    action [:enable, :start]
  end

end

Final Cookbook Directory

When finished adding the templates and building the custom resource, the cookbook directory structure should look like this:

/website
  metadata.rb
  /recipes
    default.rb
  README.md
  /resources
    httpd.rb
  /templates
    httpd.conf.erb
    httpd.service.erb

Recipe

The custom resource name is inferred from the name of the cookbook (website), the name of the resource file (httpd), and is separated by an underscore(_): website_httpd. The custom resource may be used in a recipe.

website_httpd 'httpd_site' do
  port 81
  action :create
end

which does the following:

  • Installs Apache httpd
  • Assigns an instance name of httpd_site that uses port 81
  • Configures httpd and systemd from a template
  • Creates the virtual host for the website
  • Starts the website using systemd

Custom Resource DSL

The following sections describe additional Custom Resource DSL methods that were not used in the preceding scenario:

action_class

Use the action_class block to make methods available to the actions in the custom resource. Modules with helper methods created as files in the cookbook library directory may be included. New action methods may also be defined directly in the action_class block. Code in the action_class block has access to the new_resource properties.

Assume a helper module has been created in the cookbook libraries/helper.rb file.

module Sample
  module Helper
    def helper_method
      # code
    end
  end
end

Methods may be made available to the custom resource actions by using an action_class block.

property file, String

action :delete do
  helper_method
  FileUtils.rm(new_resource.file) if file_exist
end

action_class do

  def file_exist
    ::File.exist?(new_resource.file)
  end

  require 'fileutils'

  include Sample::Helper

end

converge_if_changed

Use the converge_if_changed method inside an action block in a custom resource to compare the desired property values against the current property values (as loaded by the load_current_value method). Use the converge_if_changed method to ensure that updates only occur when property values on the system are not the desired property values and to otherwise prevent a resource from being converged.

To use the converge_if_changed method, wrap it around the part of a recipe or custom resource that should only be converged when the current state is not the desired state:

action :some_action do
  converge_if_changed do
    # some property
  end
end

For example, a custom resource defines two properties (content and path) and a single action (:create). Use the load_current_value method to load the property value to be compared, and then use the converge_if_changed method to tell Chef Infra Client what to do if that value is not the desired value:

property :content, String
property :path, String, name_property: true

load_current_value do
  if ::File.exist?(path)
    content IO.read(path)
  end
end

action :create do
  converge_if_changed do
    IO.write(new_resource.path, new_resource.content)
  end
end

When the file does not exist, the IO.write(new_resource.path, new_resource.content) code is executed and the Chef Infra Client output will print something similar to:

Recipe: recipe_name::block
  * resource_name[blah] action create
    - update my_file[blah]
    -   set content to "hola mundo" (was "hello world")

Multiple Properties

The converge_if_changed method may be used multiple times. The following example shows how to use the converge_if_changed method to compare the multiple desired property values against the current property values (as loaded by the load_current_value method).

property :path, String
property :content, String
property :mode, String

load_current_value do |desired|
  if ::File.exist?(desired.path)
    content IO.read(desired.path)
    mode ::File.stat(desired.path).mode
  end
end

action :create do
  converge_if_changed :content do
    IO.write(new_resource.path, new_resource.content)
  end
  converge_if_changed :mode do
    ::File.chmod(new_resource.mode, new_resource.path)
  end
end

where

  • load_current_value loads the property values for both content and mode
  • A converge_if_changed block tests only content
  • A converge_if_changed block tests only mode

Chef Infra Client will only update the property values that require updates and will not make changes when the property values are already in the desired state

default_action

The default action in a custom resource is, by default, the first action listed in the custom resource. For example, action aaaaa is the default resource:

property :property_name, RubyType, default: 'value'

...

action :aaaaa do
 # the first action listed in the custom resource
end

action :bbbbb do
 # the second action listed in the custom resource
end

The default_action method may also be used to specify the default action. For example:

property :property_name, RubyType, default: 'value'

default_action :aaaaa

action :aaaaa do
 # the first action listed in the custom resource
end

action :bbbbb do
 # the second action listed in the custom resource
end

defines action aaaaa as the default action. If default_action :bbbbb is specified, then action bbbbb is the default action. Use this method for clarity in custom resources, if deliberately stating the default resource is desired, or to specify a default action that is not listed first in the custom resource.

load_current_value

Use the load_current_value method to load the specified property values from the node, and then use those values when the resource is converged. This method may take a block argument.

property :path, String
property :content, String
property :mode, String

load_current_value do |new_resource|
  if ::File.exist?(new_resource.path)
    content IO.read(new_resource.path)
    mode ::File.stat(new_resource.path).mode
  end
end

Use the load_current_value method to guard against property values being replaced. For example:

property :homepage, String
property :page_not_found, String

load_current_value do
  if ::File.exist?('/var/www/html/index.html')
    homepage IO.read('/var/www/html/index.html')
  end

  if ::File.exist?('/var/www/html/404.html')
    page_not_found IO.read('/var/www/html/404.html')
  end
end

This ensures the values for homepage and page_not_found are not changed to the default values when Chef Infra Client configures the node.

new_resource.property

Custom resources are designed to use core resources that are built into Chef. In some cases, it may be necessary to specify a property in the custom resource that is the same as a property in a core resource, for the purpose of overriding that property when used with the custom resource. For example:

property :command, String, name_property: true
property :version, String

# Useful properties from the `execute` resource
property :cwd, String
property :environment, Hash, default: {}
property :user, [String, Integer]
property :sensitive, [true, false], default: false

prefix = '/opt/languages/node'

load_current_value do
  current_value_does_not_exist! if node.run_state['nodejs'].nil?
  version node.run_state['nodejs'][:version]
end

action :run do
  execute 'execute-node' do
    cwd cwd
    environment environment
    user user
    sensitive sensitive
    # gsub replaces 10+ spaces at the beginning of the line with nothing
    command <<-CODE.gsub(/^ {10}/, '')
      #{prefix}/#{new_resource.version}/#{command}
    CODE
  end
end

where the property :cwd, property :environment, property :user, and property :sensitive are identical to properties in the execute resource, embedded as part of the action :run action. Because both the custom properties and the execute properties are identical, this will result in an error message similar to:

ArgumentError
-------------
wrong number of arguments (0 for 1)

To prevent this behavior, use new_resource. to tell Chef Infra Client to process the properties from the core resource instead of the properties in the custom resource. For example:

property :command, String, name_property: true
property :version, String

# Useful properties from the `execute` resource
property :cwd, String
property :environment, Hash, default: {}
property :user, [String, Integer]
property :sensitive, [true, false], default: false

prefix = '/opt/languages/node'

load_current_value do
  current_value_does_not_exist! if node.run_state['nodejs'].nil?
  version node.run_state['nodejs'][:version]
end

action :run do
  execute 'execute-node' do
    cwd new_resource.cwd
    environment new_resource.environment
    user new_resource.user
    sensitive new_resource.sensitive
    # gsub replaces 10+ spaces at the beginning of the line with nothing
    command <<-CODE.gsub(/^ {10}/, '')
      #{prefix}/#{new_resource.version}/#{new_resource.command}
    CODE
  end
end

where cwd new_resource.cwd, environment new_resource.environment, user new_resource.user, and sensitive new_resource.sensitive correctly use the properties of the execute resource and not the identically-named override properties of the custom resource.

property

Use the property method to define properties for the custom resource. The syntax is:

property :property_name, ruby_type, default: 'value', parameter: 'value'

where

  • :property_name is the name of the property
  • ruby_type is the optional Ruby type or array of types, such as String, Integer, true, or false
  • default: 'value' is the optional default value loaded into the resource
  • parameter: 'value' optional parameters

For example, the following properties define username and password properties with no default values specified:

property :username, String
property :password, String

ruby_type

The property ruby_type is a positional parameter. Use to ensure a property value is of a particular ruby class, such as true, false, nil, String, Array, Hash, Integer, Symbol. Use an array of ruby classes to allow a value to be of more than one type. For example:

property :aaaa, String
property :bbbb, Integer
property :cccc, Hash
property :dddd, [true, false]
property :eeee, [String, nil]
property :ffff, [Class, String, Symbol]
property :gggg, [Array, Hash]

sensitive

A property can be marked sensitive by specifying sensitive: true on the property. This prevents the contents of the property from being exported to data collection and sent to an Automate server.

Note: This feature was introduced in Chef Client 12.14.

validators

A validation parameter is used to add zero (or more) validation parameters to a property.

Parameter Description

:callbacks

Use to define a collection of unique keys and values (a ruby hash) for which the key is the error message and the value is a lambda to validate the parameter. For example:

callbacks: {
             'should be a valid non-system port' => lambda {
               |p| p > 1024 && p < 65535
             }
           }

:default

Use to specify the default value for a property. For example:

default: 'a_string_value'
default: 123456789
default: []
default: ()
default: {}

:equal_to

Use to match a value with ==. Use an array of values to match any of those values with ==. For example:

equal_to: [true, false]
equal_to: ['php', 'perl']

:regex

Use to match a value to a regular expression. For example:

regex: [ /^([a-z]|[A-Z]|[0-9]|_|-)+$/, /^\d+$/ ]

:required

Indicates that a property is required. For example:

required: true

:respond_to

Use to ensure that a value has a given method. This can be a single method name or an array of method names. For example:

respond_to: valid_encoding?

Some examples of combining validation parameters:

property :spool_name, String, regex: /$\w+/
property :enabled, equal_to: [true, false, 'true', 'false'], default: true

desired_state

Add desired_state: to set the desired state property for a resource. This value may be true or false, and all properties default to true.

  • When true, the state of the property is determined by the state of the system
  • When false, the value of the property impacts how the resource executes, but it is not determined by the state of the system.

For example, if you were to write a resource to create volumes on a cloud provider you would need define properties such as volume_name, volume_size, and volume_region. The state of these properties would determine if your resource needed to converge or not. For the resource to function you would also need to define properties such as cloud_login and cloud_password. These are necessary properties for interacting with the cloud provider, but their state has no impact on decision to converge the resource or not, so you would set desired_state to false for these properties.

property :volume_name, String
property :volume_size, Integer
property :volume_region, String
property :cloud_login, String, desired_state: false
property :cloud_password, String, desired_state: false

identity

Add identity: to set a resource to a particular set of properties. This value may be true or false.

  • When true, data for that property is returned as part of the resource data set and may be available to external applications, such as reporting
  • When false, no data for that property is returned.

If no properties are marked true, the property that defaults to the name of the resource is marked true.

For example, the following properties define username and password properties with no default values specified, but with identity set to true for the user name:

property :username, String, identity: true
property :password, String

Block Arguments

Any properties that are marked identity: true, desired_state: false, or name_property: true will be directly available from load_current_value. If access to other properties of a resource is needed, use a block argument with load_current_value. The block argument will have the values of the requested resource. For example:

// Property is directly available example
property :action, String, name_property: true
property :content, String

load_current_value do |desired|
  puts "The user requested action = #{action} in the resource"
  puts "The user typed content = #{desired.content} in the resource"
end
// Block argument example
property :action, String
property :content, String

load_current_value do |desired|
  puts "The user requested action = #{desired.action} in the resource"
  puts "The user typed content = #{desired.content} in the resource"
end

property_is_set?

Use the property_is_set? method to check if the value for a property is set. The syntax is:

property_is_set?(:property_name)

The property_is_set? method will return true if the property is set.

For example, the following custom resource creates and/or updates user properties, but not their password. The property_is_set? method checks if the user has specified a password and then tells Chef Infra Client what to do if the password is not identical:

action :create do
  converge_if_changed do
    shell_out!("rabbitmqctl create_or_update_user #{username} --prop1 #{prop1} ... ")
  end

  if property_is_set?(:password)
    if shell_out("rabbitmqctl authenticate_user #{username}#{password}").error?
      converge_by "Updating password for user #{username} ..." do
        shell_out!("rabbitmqctl update_user #{username} --password #{password}")
      end
    end
  end
end

provides

Use the provides method to associate a custom resource with the Recipe DSL on different operating systems. When multiple custom resources use the same DSL, specificity rules are applied to determine the priority, from highest to lowest:

  1. provides :my_custom_resource, platform_version: ‘0.1.2’
  2. provides :my_custom_resource, platform: ‘platform_name’
  3. provides :my_custom_resource, platform_family: ‘platform_family’
  4. provides :my_custom_resource, os: ‘operating_system’
  5. provides :my_custom_resource

For example:

provides :my_custom_resource, platform: 'redhat' do |node|
  node['platform_version'].to_i >= 7
end

provides :my_custom_resource, platform: 'redhat'

provides :my_custom_resource, platform_family: 'rhel'

provides :my_custom_resource, os: 'linux'

provides :my_custom_resource

This allows you to use multiple custom resources files that provide the same resource to the user, but for different operating systems or operation system versions. With this you can eliminate the need for platform or platform version logic within your resources.

reset_property

Use the reset_property method to clear the value for a property as if it had never been set, and then use the default value. For example, to clear the value for a property named password:

reset_property(:password)

coerce

coerce is used to transform user input into a canonical form. The value is passed in, and the transformed value returned as output. Lazy values will not be passed to this method until after they are evaluated.

coerce is run in the context of the instance, which gives it access to other properties.

property :mode, coerce: proc { |m| m.is_a?(String) ? m.to_s(8) : m }

© Chef Software, Inc.
Licensed under the Creative Commons Attribution 3.0 Unported License.
The Chef™ Mark and Chef Logo are either registered trademarks/service marks or trademarks/servicemarks of Chef, in the United States and other countries and are used with Chef Inc's permission.
We are not affiliated with, endorsed or sponsored by Chef Inc.
https://docs.chef.io/custom_resources/