Best practice for simplifying cookbook attribute overrides

I need help coming up with the best “interface” for consumers of my cookbook.

Let’s say I have the following in my cookbook’s attributes/default.rb

default['my_cookbook']['my_default_hashes'] = {
  'hash1' = {
    'property1_1' => 'value1_1',
    'property1_2' => 'value1_2'
  },
  'hash2' = {
    'property2_1' => 'value2_1',
    'property2_2' => 'value2_2'
  },
  'hash3' = {
    'property3_1' => 'value3_1',
    'property3_2' => 'value3_2'
  }
}

default['my_cookbook']['my_selected_hashes'] = {}  # empty by default

I want to use node['my_cookbook']['my_selected_hashes'] as input to one of my template .erb files. I have another cookbook, let’s call it wrapper_cookbook, that consumes my_cookbook. Before I converge wrapper_cookbook, I want to set node['my_cookbook']['my_selected_hashes'] with hash1 and hash3 (but not hash2) and ALSO add a brand-new hash called hash4. And to further complicate things, I want to use a different value for property3_1 (from hash3), while keeping node['my_cookbook']['my_default_hashes'] unmodified (I’m treating it as read-only).

What is the best way to do this?

Here are some ideas of what node['my_cookbook']['my_selected_hashes'] would look like in wrapper_cookbook:
Idea #1:

node.set['my_cookbook']['my_selected_hashes'] = {
  'hash1' = {}, # left empty to indicate that I want the default values from node['my_cookbook']['my_default_hashes']['hash1']
  'hash3' = {
    'property3_1' = 'some_new_value' # this is the only attribute from hash3 I want to override 
  },
  'hash4' = {
    'property4_1' => 'value4_1',
    'property4_2' => 'value4_2'
  }
}

Idea #2:

# A new node attribute not in the original attributes/default.rb file: This is an array of my selected hashes from node['my_cookbook']['my_default_hashes']
node.set['my_cookbook']['predefined_hashes_i_want'] = ['hash1', 'hash3'] 

# This is also a new node attribute not in the original attributes/default.rb file.
node.set['my_cookbook']['my_default_hashes_overrides'] = {
  'hash3' = {
    'property3_1' = 'some_new_value' # this is the only attribute from hash3 I want to override 
  },
}

node.set['my_cookbook']['my_selected_hashes'] = {
  'hash4' = { 
    'property4_1' => 'value4_1',
    'property4_2' => 'value4_2'
  }
}

The idea above is that the hashes named in node['my_cookbook']['predefined_hashes_i_want'] would be added to node[‘my_cookbook’][‘my_selected_hashes’] while ensuring that hash3 in node['my_cookbook']['my_selected_hashes'] has a new value for its property3_1.

Which of the 2 above is better? Is there a third option that would be the best approach? I appreciate the help!

Hi,

You might be over thinking this.

The node object is a Hashie/Mash.

Your code can be written in your cookbook like this

default['my_cookbook']['my_default_hashes'] = {
  'hash1' => {
    'property1_1' => 'value1_1',
    'property1_2' => 'value1_2'
  },
  'hash2' => {
    'property2_1' => 'value2_1',
    'property2_2' => 'value2_2'
  },
  'hash3' => {
    'property3_1' => 'value3_1',
    'property3_2' => 'value3_2'
  }
}

And accessed in a wrapper cookbook like this

default['my_cookbook']['my_default_hashes']['hash2']['property2_1'] = "New Value"

My view would be to offer a single set of values that could be overwritten in the wrapper cookbook, your template then just needs to reference a single part of the Hashie/Mash.

node['my_cookbook']['my_default_hashes'].each do | hsh| ...

In your wrapper you just remove what you don’t want, for example if [“hash2”][“property2_1”] needs to be removed it can be done with remove/delete

default[''my_cookbook']['my_default_hashes']['hash2'].delete('property2_1')

As regards a “Read Only” value, I’m not sure this is possible with node, normally in Ruby you will freeze an object.

EDIT: Ok, just quickly tried this, maybe you can freeze the sub hashes

default['my_cookbook']['my_default_hashes']['hash2'].freeze
default['my_cookbook']['my_default_hashes']['hash2']['property2_1'] = "New Value"

Results in

RuntimeError
------------
can't modify frozen Chef::Node::VividMash

Additional information:
-----------------------
      Ruby objects are often frozen to prevent further modifications
      when they would negatively impact the process (e.g. values inside
      Ruby's ENV class) or to prevent polluting other objects when default
      values are passed by reference to many instances of an object (e.g.
      the empty Array as a Chef resource default, passed by reference
      to every instance of the resource).

      Chef uses Object#freeze to ensure the default values of properties
      inside Chef resources are not modified, so that when a new instance
      of a Chef resource is created, and Object#dup copies values by
      reference, the new resource is not receiving a default value that
      has been by a previous instance of that resource.

      Instead of modifying an object that contains a default value for all
      instances of a Chef resource, create a new object and assign it to
      the resource's parameter, e.g.:

      fruit_basket = resource(:fruit_basket, 'default')

      # BAD: modifies 'contents' object for all new fruit_basket instances
      fruit_basket.contents << 'apple'

      # GOOD: allocates new array only owned by this fruit_basket instance
      fruit_basket.contents %w(apple)

Take a look at the Supermarket cookbook. I really dig how they handle
merging attributes:
https://github.com/chef-cookbooks/supermarket-omnibus-cookbook/blob/master/libraries/supermarket_server.rb#L86

Maybe that’ll give you some ideas.

Thanks for the response, @chris_sullivan. Let me clarify my scenario a little bit.
Let’s say my_cookbook is an application server cookbook which allows multiple containers. The wrapper_cookbook’s goal is to create 2 containers.

container1 wants to simply use node[''my_cookbook']['my_default_hashes']['hash1'] and node[''my_cookbook']['my_default_hashes']['hash2'] without making changes to the default hashes.

container2, on the other hand, is the scenario I described in my original post where it wants to use hash1, hash3 (which should include overriding one of its properties) and add a new hash hash4.

Given the above, I do not want to make changes to node[''my_cookbook']['my_default_hashes'] because it will affect both container1 and container2.