Project: Chef Infra (Provisioning / Automation & Configuration)
File: resource_load_and_converge_methods.md

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 `loadcurrentvalue` and `convergeifchanged` 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 `loadcurrentvalue` 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 `loadcurrentresource` 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 `loadcurrentvalue` on the new instance. ```ruby class File < Chef::Resource property :path, name_attribute: true property :mode, default: 0666 property :content loadcurrentvalue do currentvaluedoesnotexist! unless File.exist?(path) mode File.stat(path).mode content IO.read(path) end action :create do convergeifchanged do File.chmod(mode, path) IO.write(path, content) end end end file '/x.txt' do 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 `loadcurrentvalue` raises `Chef::Exceptions::ActualValueDoesNotExist`, the new resource will be discarded and `currentresource` becomes `nil`. The `currentvaluedoesnot_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. ```ruby loadcurrentvalue do currentvaluedoesnotexist! if !File.exist?(path) 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 `loadcurrentvalue!` will call the superclass's `loadcurrentvalue!` method. The new resource is created with all properties copied over except desired state properties (properties in `ResourceClass.stateproperties`). This means `name`, and properties with `identity: true` or `desiredstate: false` are copied over. Normal `property` and `attribute` are not. ```ruby class DataBagItem < Chef::Resource attribute :itemname, nameattribute: true attribute :databagname, identity: true attribute :recursivelydelete, desiredstate: false attribute :data def loadcurrentvalue! data Chef::DataBagItem.new(databagname, item_name).data end end ``` The new `convergeifchanged do ... end` syntax is added to actions, which enables a lot of help for resource writers to make safe, effective

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 `convergeifchanged` block if why-run is enabled). ```ruby class File < Chef::Resource property :path, name_attribute: true property :content loadcurrentvalue do currentvaluedoesnotexist! unless File.exist?(path) content IO.read(path) end action :create do convergeifchanged do IO.write(path, content) end end end ``` Here is a sample `convergeifchanged` statement from a hypothetical FooBarBaz resource with properties `foo`, `bar` and `baz`: ```ruby convergeifchanged do if current_resource FooBarBaz.update(newresource.id, newresource.foo, newresource.bar, newresource.baz) else FooBarBaz.create(newresource.id, newresource.foo, newresource.bar, newresource.baz) end end ``` This is what you would have to write to do the equivalent: ```ruby if current_resource differences = [] if (newresource.propertyisset?(:foo) && newresource.foo != current_resource.foo) differences << "foo = #{new_resource.foo}" end if (newresource.propertyisset?(:bar) && newresource.bar != current_resource.bar) differences << "bar = #{new_resource.bar}" end if (newresource.propertyisset?(:baz) && newresource.baz != current_resource.baz) differences << "baz = #{new_resource.baz}" end if !differences.empty? convergeby "updating FooBarBaz #{newresource.id}, setting #{differences.join(", ")}" do FooBarBaz.create(newresource.id, newresource.foo, newresource.bar, newresource.baz) end end else convergeby "creating FooBarBaz #{newresource.id} with foo = #{newresource.foo}, bar = #{newresource.bar}, baz = #{new_resource.baz}" do FooBarBaz.update(newresource.id, newresource.foo, newresource.bar, newresource.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, and 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 loadcurrentvalue succeeded, and the default value if we are creating a new resource (if `loadcurrentvalue` raised `ActualValueDoesNotExist`). ```ruby class File < Chef::Resource property :path, name_attribute: true property :mode, default: 0666 property :content loadcurrentvalue do currentvaluedoesnotexist! unless File.exist?(path) mode File.stat(path).mode content IO.read(path) end action :create do convergeifchanged do File.chmod(mode, path) IO.write(path, content) end end end file '/x.txt' do 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 `propertyisset?(: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. `convergeifchanged :attribute1, :attribute2, ... do` allows the user to target different groups of changes based on exactly which attributes have changed: ```ruby class File < Chef::Resource property :path, name_attribute: true property :mode property :content loadcurrentvalue do currentvaluedoesnotexist! unless File.exist?(path) mode File.stat(path).mode content IO.read(path) end action :create do convergeifchanged :mode do File.chmod(mode, path) end convergeifchanged :content do IO.write(path, content) end end end ```