diff options
author | John Keiser <john@johnkeiser.com> | 2015-07-27 09:38:47 -0600 |
---|---|---|
committer | John Keiser <john@johnkeiser.com> | 2015-07-27 09:38:47 -0600 |
commit | 28e20d62f3dcb29f34c2f67b41a1b486108f388d (patch) | |
tree | 75b842d58b932412e8d6d34ed9fe48053169d83f | |
parent | ca8a8bdb000c7cb82588cc56ddfb32464babf3c3 (diff) | |
parent | fbb483ec880264262494e908a00445c007aacc59 (diff) | |
download | chef-28e20d62f3dcb29f34c2f67b41a1b486108f388d.tar.gz |
Merge branch 'jk/resource_load'
-rw-r--r-- | DOC_CHANGES.md | 268 | ||||
-rw-r--r-- | lib/chef/property.rb | 2 | ||||
-rw-r--r-- | lib/chef/provider.rb | 21 | ||||
-rw-r--r-- | lib/chef/resource.rb | 55 | ||||
-rw-r--r-- | spec/integration/recipes/resource_load_spec.rb | 206 |
5 files changed, 548 insertions, 4 deletions
diff --git a/DOC_CHANGES.md b/DOC_CHANGES.md index 92a178e20b..2073b4c089 100644 --- a/DOC_CHANGES.md +++ b/DOC_CHANGES.md @@ -22,3 +22,271 @@ will set the node's environment to `"pre-production"`. > *Note that the environment specified by `chef_environment` in your JSON will take precedence over an environment specified by `-E ENVIROMENT` when both options are provided.* + +### Resources Made Easy + +Resources are central to Chef. The system is extensible so that you can write +your own reusable resources, and use them in your recipes, and even publish them +so that others can use them too! + +However, writing these resources has not been as easy as we would have liked. In +Chef 12.5, we are fixing this with a large number of DSL improvements designed +to reduce the number of things you need to type and think about when you create +a resource. Resources should be your go-to solution for many Chef problems, and +these changes make them easy enough to dash off in an instant, while retaining +all the power you're accustomed to. + +The process to create a resource is now: + +1. Make a resource file in your cookbook (like `resources/my_resource.rb`). +2. Add the recipes defining your actions using the `action :create <recipe>` DSL. +3. Add properties so the user can tweak some knobs on your resource (like paths, + or preferences), using the `property :my_property, <type>, <options>` DSL. +4. Use the resource in your recipe! + +There are other things you can do, but this is the most basic (and the first) +thing you will start with. + +Let's demonstrate the new features by taking a simple recipe from the awesome +[learnchef tutorial](https://learn.chef.io/learn-the-basics/rhel/configure-a-package-and-service/), +and turning it into a reusable resource: + +```ruby +package 'httpd' + +service 'httpd' do + action [:enable, :start] +end + +file '/var/www/html/index.html' do + content '<html> + <body> + <h1>hello world</h1> + </body> +</html>' +end + +service 'iptables' do + action :stop +end +``` + +We'll design a resource that lets you write this recipe instead: + +```ruby +single_page_website 'mysite' do + homepage '<html> + <body> + <h1>hello world</h1> + </body> + </html>' +end +``` + +#### Declaring the Resource + +The first thing we do is declare the resource. We can do that by creating an +empty file, `resources/single_page_website.rb`, in our cookbook. + +When you do this, the `single_page_website` resource will work in all recipes! + +```ruby +single_page_website 'mysite' +``` + +It won't do anything yet, though :) + +#### Declaring an Action + +Let's make our resource do something. To start with, we'll just have it do exactly +what the learnchef tutorial does, but in the resource. Put this in +`resources/single_page_website.rb`: + +```ruby +action :create do + package 'httpd' + + service 'httpd' do + action [:enable, :start] + end + + file '/var/www/html/index.html' do + content '<html> + <body> + <h1>hello world</h1> + </body> + </html>' + end + + service 'iptables' do + action :stop + end +end +``` + +Now, your simple recipe can use this resource to do what learnchef did: + +```ruby +single_page_website 'mysite' +``` + +We've got ourselves an httpd! + +You will notice the only thing we've done is to add `action :create` around the +recipe. The `action` keyword lets you declare a recipe inline, which will be +executed when the user uses your resource in a recipe. + +#### Declaring a resource property: "homepage" + +This isn't super reusable yet--you might want your webpage to say something other +than "hello world". Let's add a couple of properties for that, by putting this +at the top of `resources/single_page_website`, and modifying the recipe to use +"title" and "body": + +```ruby +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 homepage + end + + service 'iptables' do + action :stop + end +end +``` + +Now you can run this recipe: + +```ruby +single_page_website 'mysite' do + homepage '<h1>My own page</h1>' +end +``` + +And you've got a website with your stuff! + +What you've done here is add *properties*. Properties are the *desired state* of +a resource, in this case, `homepage` defines the text on the website. When you +add a property, you're letting a user give it whatever value they want. + +When you define a property, there are three bits: +`property :<name>, <type>, <options>`. *Name* defines the name of the property, +so that people can set the property using `name <value>` when they use your +resource. *Type* defines the type of the property: for example, String, Integer +and Array are all possible types. Type is optional. *Options* define a large +number of validation and other options. You've seen `default` already now, +but there are a ton of others. + +#### Adding another property: "not_found_page" + +What if we want a custom 404 page for when people try to go to other pages in +our website? Let's add one more property, to make this even nicer: + +```ruby +property :homepage, String, default: '<h1>hello world</h1>' +property :not_found_page, String, default: '<h1>No such page! Sorry. 404.</h1>' + +action :create do + package 'httpd' + + service 'httpd' do + action [:enable, :start] + end + + file '/var/www/html/index.html' do + content homepage + end + + # These together tell Apache to use your custom 404 page: + file '/var/www/html/404.html' do + content not_found_page + end + file '/var/www/html/.htaccess' do + content 'ErrorDocument 404 /404.html' + end + + service 'iptables' do + action :stop + end +end +``` + +Now you can run this recipe: + +```ruby +single_page_website 'mysite' do + homepage '<h1>My own page</h1>' + not_found_page '<h1>Grr. Page not found. Sorry. (404)</h1>' +end +``` + +#### Adding another action: "stop" + +What if we want to stop the website? Just add another action into the bottom of +`resources/single_page_website.rb`: + +```ruby +action :stop do + service 'httpd' do + action :stop + end +end +``` + +This action looks a lot like the other. + +There are a ton of other things you can do to create resources, but this should +give you a pretty basic idea. + +### Advanced Resource Capabilities + +#### Ruby Developers: Resources as Classes + +If you are a Ruby developer, we've made it easier to create a Resource outside +of a cookbook (or in a library) by declaring a class! Declare +`class SinglePageWebsite < Chef::Resource` and put the entire resource +declaration inside, and the `single_page_website` resource will work! + +#### Reading the current value: load_current_value + +There is a pitfall inherent in a resource, where users will sometimes omit a +property from a resource, and become surprised when the system overwrites it +with the default! For example, if your website already exists, this recipe +will replace the *homepage* with "hello world": + +```ruby +single_page_website 'mysite' do + not_found_page '<h1>nice</h1>' +end +``` + +It's not at all clear that that's what the user wanted--they didn't say anything +about the homepage, so why did something happen to it? + +To guard against this, you can implement `load_current_value` in your resource. +Put this in `resources/single_page_website.rb`: + +```ruby +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') + not_found_page IO.read('/var/www/html/404.html') + end +end +``` + +Now, the above recipe knows what the current homepage is, and will not change it! + +This capability is also used for several other things, including reporting (to +describe what changed) and deeper custom resources (ones that don't use recipes, +which we'll cover later). diff --git a/lib/chef/property.rb b/lib/chef/property.rb index 408090d37b..6579eb06a8 100644 --- a/lib/chef/property.rb +++ b/lib/chef/property.rb @@ -86,7 +86,7 @@ class Chef # def initialize(**options) options.each { |k,v| options[k.to_sym] = v if k.is_a?(String) } - options[:name_property] = options.delete(:name_attribute) unless options.has_key?(:name_property) + options[:name_property] = options.delete(:name_attribute) if options.has_key?(:name_attribute) && !options.has_key?(:name_property) @options = options if options.has_key?(:default) diff --git a/lib/chef/provider.rb b/lib/chef/provider.rb index dcfc92645b..43d58740f2 100644 --- a/lib/chef/provider.rb +++ b/lib/chef/provider.rb @@ -211,10 +211,29 @@ class Chef extend Forwardable define_singleton_method(:to_s) { "#{resource_class} forwarder module" } define_singleton_method(:inspect) { to_s } + # Add a delegator for each explicit property that will get the *current* value + # of the property by default instead of the *actual* value. + resource.class.properties.each do |name, property| + class_eval(<<-EOM, __FILE__, __LINE__) + def #{name}(*args, &block) + # If no arguments were passed, we process "get" by defaulting + # the value to current_resource, not new_resource. This helps + # avoid issues where resources accidentally overwrite perfectly + # valid stuff with default values. + if args.empty? && !block + if !new_resource.property_is_set?(__method__) && current_resource + return current_resource.public_send(__method__) + end + end + new_resource.public_send(__method__, *args, &block) + end + EOM + end dsl_methods = resource.class.public_instance_methods + resource.class.protected_instance_methods - - provider_class.instance_methods + provider_class.instance_methods - + resource.class.properties.keys def_delegators(:new_resource, *dsl_methods) end include @included_resource_dsl_module diff --git a/lib/chef/resource.rb b/lib/chef/resource.rb index dca0033409..bb5c6050fe 100644 --- a/lib/chef/resource.rb +++ b/lib/chef/resource.rb @@ -1375,6 +1375,34 @@ class Chef end # + # Define a method to load up this resource's properties with the current + # actual values. + # + # @param load_block The block to load. Will be run in the context of a newly + # created resource with its identity values filled in. + # + def self.load_current_value(&load_block) + define_method(:load_current_value!, &load_block) + end + + # + # Get the current actual value of this resource. + # + # This does not cache--a new value will be returned each time. + # + # @return A new copy of the resource, with values filled in from the actual + # current value. + # + def current_resource + provider = provider_for_action(Array(action).first) + if provider.whyrun_mode? && !provider.whyrun_supported? + raise "Cannot retrieve #{self.class.current_resource} in why-run mode: #{provider} does not support why-run" + end + provider.load_current_resource + provider.current_resource + end + + # # The action provider class is an automatic `Provider` created to handle # actions declared by `action :x do ... end`. # @@ -1414,8 +1442,31 @@ class Chef use_inline_resources include_resource_dsl true define_singleton_method(:to_s) { "#{resource_class} action provider" } - define_singleton_method(:inspect) { to_s } - define_method(:load_current_resource) {} + def self.inspect + to_s + end + def load_current_resource + if new_resource.respond_to?(:load_current_value!) + # dup the resource and then reset desired-state properties. + current_resource = new_resource.dup + + # We clear desired state in the copy, because it is supposed to be actual state. + # We keep identity properties and non-desired-state, which are assumed to be + # "control" values like `recurse: true` + current_resource.class.properties.each do |name,property| + if property.desired_state? && !property.identity? && !property.name_property? + property.reset(current_resource) + end + end + + if current_resource.method(:load_current_value!).arity > 0 + current_resource.load_current_value!(new_resource) + else + current_resource.load_current_value! + end + end + @current_resource = current_resource + end end end diff --git a/spec/integration/recipes/resource_load_spec.rb b/spec/integration/recipes/resource_load_spec.rb new file mode 100644 index 0000000000..c29b877b59 --- /dev/null +++ b/spec/integration/recipes/resource_load_spec.rb @@ -0,0 +1,206 @@ +require 'support/shared/integration/integration_helper' + +describe "Resource.load_current_value" do + include IntegrationSupport + + module Namer + extend self + attr_accessor :current_index + def incrementing_value + @incrementing_value += 1 + @incrementing_value + end + attr_writer :incrementing_value + end + + before(:all) { Namer.current_index = 1 } + before { Namer.current_index += 1 } + before { Namer.incrementing_value = 0 } + + let(:resource_name) { :"load_current_value_dsl#{Namer.current_index}" } + let(:resource_class) { + result = Class.new(Chef::Resource) do + def self.to_s; resource_name; end + def self.inspect; resource_name.inspect; end + property :x, default: lazy { "default #{Namer.incrementing_value}" } + def self.created_x=(value) + @created = value + end + def self.created_x + @created + end + action :create do + new_resource.class.created_x = x + end + end + result.resource_name resource_name + result + } + + # Pull on resource_class to initialize it + before { resource_class } + + context "with a resource with load_current_value" do + before :each do + resource_class.load_current_value do + x "loaded #{Namer.incrementing_value} (#{self.class.properties.sort_by { |name,p| name }. + select { |name,p| p.is_set?(self) }. + map { |name,p| "#{name}=#{p.get(self)}" }. + join(", ") })" + end + end + + context "and a resource with x set to a desired value" do + let(:resource) do + e = self + r = nil + converge { + r = public_send(e.resource_name, 'blah') do + x 'desired' + end + } + r + end + + it "current_resource is passed name but not x" do + expect(resource.current_resource.x).to eq 'loaded 2 (name=blah)' + end + + it "resource.current_resource returns a different resource" do + expect(resource.current_resource.x).to eq 'loaded 2 (name=blah)' + expect(resource.x).to eq 'desired' + end + + it "resource.current_resource constructs the resource anew each time" do + expect(resource.current_resource.x).to eq 'loaded 2 (name=blah)' + expect(resource.current_resource.x).to eq 'loaded 3 (name=blah)' + end + + it "the provider accesses the current value of x" do + expect(resource.class.created_x).to eq 'desired' + end + + context "and identity: :i and :d with desired_state: false" do + before { + resource_class.class_eval do + property :i, identity: true + property :d, desired_state: false + end + } + + before { + resource.i 'desired_i' + resource.d 'desired_d' + } + + it "i, name and d are passed to load_current_value, but not x" do + expect(resource.current_resource.x).to eq 'loaded 2 (d=desired_d, i=desired_i, name=blah)' + end + end + + context "and name_property: :i and :d with desired_state: false" do + before { + resource_class.class_eval do + property :i, name_property: true + property :d, desired_state: false + end + } + + before { + resource.i 'desired_i' + resource.d 'desired_d' + } + + it "i, name and d are passed to load_current_value, but not x" do + expect(resource.current_resource.x).to eq 'loaded 2 (d=desired_d, i=desired_i, name=blah)' + end + end + end + + context "and a resource with no values set" do + let(:resource) do + e = self + r = nil + converge { + r = public_send(e.resource_name, 'blah') do + end + } + r + end + + it "the provider accesses values from load_current_value" do + expect(resource.class.created_x).to eq 'loaded 1 (name=blah)' + end + end + + let (:subresource_name) { + :"load_current_value_subresource_dsl#{Namer.current_index}" + } + let (:subresource_class) { + r = Class.new(resource_class) do + property :y, default: lazy { "default_y #{Namer.incrementing_value}" } + end + r.resource_name subresource_name + r + } + + # Pull on subresource_class to initialize it + before { subresource_class } + + let(:subresource) do + e = self + r = nil + converge { + r = public_send(e.subresource_name, 'blah') do + x 'desired' + end + } + r + end + + context "and a child resource class with no load_current_value" do + it "the parent load_current_value is used" do + expect(subresource.current_resource.x).to eq 'loaded 2 (name=blah)' + end + it "load_current_value yields a copy of the child class" do + expect(subresource.current_resource).to be_kind_of(subresource_class) + end + end + + context "And a child resource class with load_current_value" do + before { + subresource_class.load_current_value do + y "loaded_y #{Namer.incrementing_value} (#{self.class.properties.sort_by { |name,p| name }. + select { |name,p| p.is_set?(self) }. + map { |name,p| "#{name}=#{p.get(self)}" }. + join(", ") })" + end + } + + it "the overridden load_current_value is used" do + current_resource = subresource.current_resource + expect(current_resource.x).to eq 'default 3' + expect(current_resource.y).to eq 'loaded_y 2 (name=blah)' + end + end + + context "and a child resource class with load_current_value calling super()" do + before { + subresource_class.load_current_value do + super() + y "loaded_y #{Namer.incrementing_value} (#{self.class.properties.sort_by { |name,p| name }. + select { |name,p| p.is_set?(self) }. + map { |name,p| "#{name}=#{p.get(self)}" }. + join(", ") })" + end + } + + it "the original load_current_value is called as well as the child one" do + current_resource = subresource.current_resource + expect(current_resource.x).to eq 'loaded 3 (name=blah)' + expect(current_resource.y).to eq 'loaded_y 4 (name=blah, x=loaded 3 (name=blah))' + end + end + end + +end |