RFC | Title | Author | Status | Type |
---|---|---|---|---|
56 |
Easy Resource Load And Converge |
John Keiser <[email protected]> |
Final |
Standards Track |
With the introduction of action
on resources, it becomes useful to have a
blessed way to get the actual value of the resource. This proposal adds
load_current_value
and converge_if_changed
to help with this purpose, enabling:
- Low-ceremony load methods (as easy to write as we can make it)
- A super easy converge model that automatically compares current vs. desired values and prints green text
As a Chef resource writer,
I want to be able to read the current value of my resource at converge time,
so that it is easy to tell the difference between current and desired value.
As a Chef resource writer,
I want a converge model that compares current and desired values for me,
So that the easiest converge to write is the most correct one.
When using action
, one needs a way to load the actual system value of the resource, so that it can be compared to the desired value and a decision made as to whether to change anything.
When the resource writer defines load_current_value
on the resource class, it can be called to load the real system value into the resource. Before any action runs, this will be used by load_current_resource
to load the resource. action
will do some important work before calling the new method:
- Create a new instance of the resource with the same name.
- Copy all non-desired-state values from the desired resource into the new instance.
- Call
load_current_value
on the new instance.
class File < Chef::Resource
property :path, name_attribute: true
property :mode, default: 0666
property :content
load_current_value do
current_value_does_not_exist! unless File.exist?(path)
mode File.stat(path).mode
content IO.read(path)
end
action :create do
converge_if_changed do
File.chmod(mode, path)
IO.write(path, content)
end
end
end
file '/x.txt' do
# Before the change, the above code would have modified `mode` to be `0666`.
# After, it leaves `mode` alone.
content 'Hello World'
end
To appropriately handle actual value loading, the user needs a way to specify that the actual value legitimately does not exist (rather than simply not filling in the object and getting nil
s in it). If load_current_value
raises Chef::Exceptions::ActualValueDoesNotExist
, the new resource will be discarded and current_resource
becomes nil
. The current_value_does_not_exist!
method can be called to raise this.
NOTE: The alternative was to have users return false
if the resource does not exist; but I didn't want users to be forced into the ceremony of a trailing true
line.
load_current_value do
# Check for existence before doing anything else.
current_value_does_not_exist! if !File.exist?(path)
# Set "mode" on the resource.
mode File.stat(path).mode
end
The block will also be passed the original (desired) resource as a parameter, in case it is needed.
super
in load_current_value!
will call the superclass's load_current_value!
method.
The new resource is created with all properties copied over except desired state properties (properties in ResourceClass.state_properties
). This means name
, and properties with identity: true
or desired_state: false
are copied over. Normal property
and attribute
are not.
class DataBagItem < Chef::Resource
# Copied
attribute :item_name, name_attribute: true
attribute :data_bag_name, identity: true
attribute :recursively_delete, desired_state: false
# Not copied:
attribute :data
def load_current_value!
data Chef::DataBagItem.new(data_bag_name, item_name).data
end
end
The new converge_if_changed do ... end
syntax is added to actions, which enables a lot of help for resource writers to make safe, effective resources. It performs several key tasks common to nearly every resource (which are often not done correctly):
- Goes through all attributes on the resource and checks whether the desired value is different from the current value.
- If any attributes are different, prints appropriate green text.
- Honors why-run (and does not call the
converge_if_changed
block if why-run is enabled).
class File < Chef::Resource
property :path, name_attribute: true
property :content
load_current_value do
current_value_does_not_exist! unless File.exist?(path)
content IO.read(path)
end
action :create do
converge_if_changed do
IO.write(path, content)
end
end
end
Here is a sample converge_if_changed
statement from a hypothetical FooBarBaz resource with properties foo
, bar
and baz
:
converge_if_changed do
if current_resource
FooBarBaz.update(new_resource.id, new_resource.foo, new_resource.bar, new_resource.baz)
else
FooBarBaz.create(new_resource.id, new_resource.foo, new_resource.bar, new_resource.baz)
end
end
This is what you would have to write to do the equivalent:
if current_resource
# We're updating; look for properties that the user wants to change (do the "test" part of test-and-set)
differences = []
if (new_resource.property_is_set?(:foo) && new_resource.foo != current_resource.foo)
differences << "foo = #{new_resource.foo}"
end
if (new_resource.property_is_set?(:bar) && new_resource.bar != current_resource.bar)
differences << "bar = #{new_resource.bar}"
end
if (new_resource.property_is_set?(:baz) && new_resource.baz != current_resource.baz)
differences << "baz = #{new_resource.baz}"
end
if !differences.empty?
converge_by "updating FooBarBaz #{new_resource.id}, setting #{differences.join(", ")}" do
FooBarBaz.create(new_resource.id, new_resource.foo, new_resource.bar, new_resource.baz)
end
end
else
# If the current resource doesn't exist, we're definitely creating it
converge_by "creating FooBarBaz #{new_resource.id} with foo = #{new_resource.foo}, bar = #{new_resource.bar}, baz = #{new_resource.baz}" do
FooBarBaz.update(new_resource.id, new_resource.foo, new_resource.bar, new_resource.baz)
end
end
The easiest way to write a resource must be the most correct one.
There is a subtle pitfall when updating a resource, where the user has set some values, but not all. One can easily end up writing a resource which will overwrite perfectly good system properties with their defaults, which can cause instability. If the user does not specify a property, it is generally preferable to preserve its existing value rather than overwrite it.
To prevent this, referencing the bare property in an action
will now yield the actual value if load_current_value succeeded, and the default value if we are creating a new resource (if load_current_value
raised ActualValueDoesNotExist
).
class File < Chef::Resource
property :path, name_attribute: true
property :mode, default: 0666
property :content
load_current_value do
current_value_does_not_exist! unless File.exist?(path)
mode File.stat(path).mode
content IO.read(path)
end
action :create do
converge_if_changed do
File.chmod(mode, path)
IO.write(path, content)
end
end
end
file '/x.txt' do
# Before the change, the above code would have modified `mode` to be `0666`.
# After, it leaves `mode` alone.
content 'Hello World'
end
There will be times when the old behavior of overwriting with defaults is desired. The resource writer can still find out whether mode
was set with property_is_set?(:mode)
, and can still access the default value with new_resource.mode
if it is not set.
There are no backwards-compatibility issues with this because it only applies to action
, which has not been released yet.
Some resources perform several different (possibly expensive) operations depending on what is set. converge_if_changed :attribute1, :attribute2, ... do
allows the user to target different groups of changes based on exactly which attributes have changed:
class File < Chef::Resource
property :path, name_attribute: true
property :mode
property :content
load_current_value do
current_value_does_not_exist! unless File.exist?(path)
mode File.stat(path).mode
content IO.read(path)
end
action :create do
converge_if_changed :mode do
File.chmod(mode, path)
end
converge_if_changed :content do
IO.write(path, content)
end
end
end
This work is in the public domain. In jurisdictions that do not allow for this, this work is available under CC0. To the extent possible under law, the person who associated CC0 with this work has waived all copyright and related or neighboring rights to this work.