Skip to main content

About Unified Mode

Unified mode is a setting that will compile and converge a custom resource’s action block in one pass and in the order that the code inside that block is composed, from beginning to end. This replaces Chef Infra’s two-pass parsing with single-pass parsing so that resources are executed as soon as they’re declared. This results in clearer code and requires less Ruby knowledge to understand the order of operations.

Availability

Unified Mode (unified_mode true) is the default behavior starting in Chef Infra Client 18 (April 2022).

See the following table for Chef Infra Client versions where Unified Mode can be enabled in custom resources:

Chef Infra ClientUnified Mode
18.x (2022)Default: unified_mode true
17.x (2021)Default: unified_mode false
16.x (2020)Default: unified_mode false
15.3 and higherDefault: unified_mode false
15.0-15.2Not available
14.14-14.15Default: unified_mode false
Lower than 14.14Not available

Enable Unified Mode

Unified Mode is enabled by default starting in Chef Infra Client 18.

In Chef Infra Client 17 (April 2021) and some earlier versions, you can enable Unified Mode in custom resources by adding unified_mode true. You can upgrade most custom resources to use Unified Mode without additional work other than testing and validation. See the following example:

# enable unified mode
unified_mode true

provides :myresource

actions :run do
  [...]
end

Unified Mode isolation

If a Unified Mode resource calls a non-Unified Mode resource, the called resource isn’t executed in Unified Mode. Each resource maintains its own state whether it’s in Unified Mode or not. You don’t need to modify a custom resource that calls a Unified Mode resource since the calling context won’t affect the resource’s execution. Resources using Unified Mode may call resources not using Unified Mode and vice versa.

Benefits of Unified Mode

Single-pass execution

In Unified Mode, the Chef Infra Language executes from top to bottom, eliminating the compile and converge phases.

With the deferred execution of resources to converge time, the user has to understand many different details of the Ruby parser to understand what constructs relate to Chef Infra resources and what constructs are parts of the core Ruby language to determine when those expression are executed. All that complexity is removed in Unified Mode.

Elimination of lazy blocks

Several aspects of the Chef Infra Language still work but are no longer necessary in Unified Mode. Unified Mode eliminates the need for lazy blocks and the need to lazy Ruby code through a Ruby block.

Rescue blocks and other Ruby constructs work correctly

In Unified Mode, it’s now easy to write a rescue wrapper around a Chef Infra resource:

begin
  execute "a command that fails" do
    command "/bin/false"
  end
rescue Mixlib::ShellOut::ShellCommandFailed => e
  raise "failing because /bin/false returned a failed exit status"
end

Examples

Basic example

A simple motivating example is to have a resource that downloads a JSON message using the remote_file resource, parse the JSON using the ruby_block, and then render a value into a file or template resource.

Without Unified Mode, correctly writing this simple resource is complicated:

provides :downloader

action :doit do
  remote_file "/tmp/users.json" do
    source "https://jsonplaceholder.typicode.com/users"
  end

  array = nil

  ruby_block "convert" do
    block do
      array = FFI_Yajl::Parser.parse(IO.read("/tmp/users.json"))
    end
  end

  file "/tmp/phone" do
    content lazy { array.last["phone"] }
  end
end

Since the remote_file and file resources execute at converge time, the Ruby code to parse the JSON needs to be wrapped in a ruby_block resource, the local variable then needs to be declared outside of that scope (requiring a deep knowledge of Ruby variable scoping rules), and then the content rendered into the file resource must be wrapped with lazy since the Ruby parses all arguments of properties at compile time instead of converge time.

Unified Mode simplifies this resource:

unified_mode true

provides :downloader

action :doit do
  remote_file "/tmp/users.json" do
    source "https://jsonplaceholder.typicode.com/users"
  end

  array = FFI_Yajl::Parser.parse(IO.read("/tmp/users.json"))

  file "/tmp/phone" do
    content array.last["phone"]
  end
end

Unified Mode eliminates the need for the ruby_block resource, the lazy evaluation, and the variable declaration, simplifying how the cookbook is authored.

Recovery and exception handling

Another advantage is in error recovery and the use of rescue.

unified_mode true

provides :redis_install

action :install do
  version = "5.0.5"

  # the downloading of this file acts as a guard for all the later
  # resources -- but if the download is successful while the later
  # resources fail for some transient issue, will won't redownload on
  # the next run -- we lose our edge trigger.
  #
  remote_file "/tmp/redis-#{version}.tar.gz" do
    source "http://download.redis.io/releases/redis-#{version}.tar.gz"
    notifies :run, "execute[unzip_redis_archive]", :immediately
  end

  begin
    execute "unzip_redis_archive" do
      command "tar xzf redis-#{version}.tar.gz"
      cwd "/tmp"
      action :nothing
      notifies :run, "execute[build_and_install_redis]", :immediately
    end

    execute "build_and_install_redis" do
      command 'make && make install'
      cwd "/tmp/redis-#{version}"
      action :nothing
    end
  rescue Exception
    file "/tmp/redis-#{version}.tar.gz" do
      action :delete
    end
    raise
  end
end

This simplified example shows how to trap exceptions from resources using normal Ruby syntax and to clean up the resource. Without Unified Mode, this syntax is impossible. Normally when the execute resources are parsed, they only create the objects in the resource_collection to later be evaluated so that no exception is thrown while Ruby is parsing the action block. Every action is delayed to the later converge phase. In Unified Mode, the resource runs when Ruby is done parsing its block, so exceptions happen in-line with Ruby parsing and the rescue clause now works as expected.

This is useful because the TAR extraction throws an exception (for example, the node could be out of disk space), which deletes the TAR file. The next time Chef Infra Client runs, the TAR file will be redownload. If the resource didn’t have file cleanup after an exception, the TAR file would remain on the client node even though the resource isn’t complete and the extraction didn’t happen, leaving the resource in a broken, indeterminate state.

Actions on Later Resources

Since Unified Mode executes your resource as it’s compiled, :immediate notifications that execute later resources are handled differently than in the past.

:immediate Notifications to Later Resources

Unified mode delays immediate notifications to later resources. In unified mode, the Chef Infra Client saves immediate notifications and executes them when the later resource is parsed. Immediate notifications to prior resources and delayed notifications behave the same as they did before unified mode.

The result of sequentially chaining immediate notifications is the same as before unified mode. Instead of immediately notifying results, the notifications fire in order as they’re parsed, which has the same outcome. If the parse order and the intended execution order are different, then the results may be different and are a reflection of the parse order.

The changes to sending immediate notification could result in subtle changes to behaviors in some resources, but it’s not a breaking change to common patterns of writing resources.

Chaining immediate notifications to later resources:

remote_file "#{Chef::Config[:file_cache_path]}/myservice.tgz" do
  source "http://acme.com/myservice.tgz"
  notifies :extract, "archive_file[myservice.tgz]", :immediately
end

archive_file "#{Chef::Config[:file_cache_path]}/myservice.tgz" do
  destination '/srv/myservice'
  notifies :start, "service[myservice]", :immediately
  action :nothing
end

service "myservice" do
  action :nothing
end

:before Notifications to Later Resources

In unified mode, you must declare a resource before sending a before notification to it.

Resources that subscribe to a before notification to a later resource must be declared after the resource that triggers the notification.

This resource declares a before notification to a later resource and will no longer work:

package "myservice" do
  notifies :stop, "service[myservice]", :before
  notifies :start, "service[myservice]", :immediately
end

service "myservice" do
  action :nothing
end

Instead, declare the resource and then declare actions. For example:

service "myservice" do
  action :nothing
end

package "myservice" do
  notifies :stop, "service[myservice]", :before
  notifies :start, "service[myservice]", :immediately
end

Out of Order Execution

Unified mode breaks custom resources that rely on the out-of-order execution of compile-time statements. Move any affected compile-time statements to the location in the code where they’re intended to execute.

Out-of-order execution is rare. Internally at Chef, none of our custom resources broke during our migration to unified mode. Instead, we discovered a few cases in which custom resource code was intended to run in order, but Chef Infra Client executed it out of order. In these cases, Unified Mode fixed errors instead of introducing bugs.

Notifications and accumulators

The accumulator pattern works unchanged. Notifications to the :root run context still behave identically. Since the compile and converge phases of custom resources both fire in the converge time (typically) of the enclosing run_context, the effect of eliminating the separate compile and converge phases of the custom resource has no visible effect from the outer context.

Troubleshooting Unified Mode

Unified mode changes the execution of a custom resource to run in one phase, in the order that the code is written, from the first line of the code to the last. Custom resources designed to use two phases may need modification. These fall into three general types:

  • Resources with changes to internal sub-resources
  • Resources with actions on later resources
  • Resources that rely on the out-of-order execution

When designing a custom resource for unified mode:

  • Declare a resource first and then declare actions on it
  • Write resources in run-time order

Resources with changes to internal sub-resources

Some custom resources are designed to create and edit other sub-resources as part of the resource declaration. In unified mode, Chef Infra Client parses a resource code block that creates or edits a sub-resource and immediately tries to apply that change, even though the sub-resource doesn’t yet exist. This results in the execution of an incomplete resource.

For example, with Unified Mode enabled, this code from the dhcp cookbook is designed to create and edit a shared dhcp_subnet resource, but it won’t work as expected:

# 'edit_resource' results in an incomplete subresource
sr = edit_resource(:dhcp_subnet, "#{new_resource.name}_sharedsubnet_#{subnet}") do
  owner new_resource.owner
  group new_resource.group
  mode new_resource.mode

  ip_version new_resource.ip_version
  conf_dir new_resource.conf_dir
  shared_network true
end

properties.each do |property, value|
  sr.send(property, value)
end

To correct custom resources that change sub-resources during their declaration, you can:

  • Apply properties in the code block (preferred)
  • Run the resource explicitly (not preferred)

Apply properties in the code block

This pattern declares the sub-resource in one code block and then changes it in the next code block. This is the preferred pattern in Unified Mode because all resources execute in order at compile time.

dhcp_subnet "#{new_resource.name}_sharedsubnet_#{subnet}" do
  owner new_resource.owner
  group new_resource.group
  mode new_resource.mode

  ip_version new_resource.ip_version
  conf_dir new_resource.conf_dir
  shared_network true

  properties.each do |property, value|
    send(property, value)
  end
end

Run the resource explicitly

Another solution is to continue saving the resource as a variable, declare action :nothing within the codeblock, and then explicitly run the action in another code block.

The pattern of saving a resource as a variable and then forcing it to run at compile time with an explicit run_action works as it has in the past, but it’s not a preferred pattern. Unified mode forces resource execution to compile time by default, which makes this pattern redundant.

sr = edit_resource(:dhcp_subnet, "#{new_resource.name}_sharedsubnet_#{subnet}") do
  owner new_resource.owner
  group new_resource.group
  mode new_resource.mode

  ip_version new_resource.ip_version
  conf_dir new_resource.conf_dir
  shared_network true

  action :nothing
end

properties.each do |property, value|
  sr.send(property, value)
end

# Run the action explicitly
sr.run_action(:create)
Edit this page on GitHub

Thank you for your feedback!

×