Search code examples
rubyattributeschef-recipechef-infra

Is there anyway to define defaults for multidimensional attribute trees in Chef?


Typical use case of default attributes

Recipe attributes:

default['human']['jack']['arms'] = 2
default['human']['jack']['legs'] = 2
default['human']['jack']['heads'] = 1

In node/role:

override['human']['jack']['legs'] = 1

Also in recipe:

node.override['human']['jack']['legs'] = 1

My use case with dynamic collections

So what if my recipe doesn't know Jack will exist for a node/role and I want a dynamic collection for lots of entries. What is a good strategy to define or merge in defaults?

I don't want to suggest a solution so I'll use a made up wildcard for my example where Jack and Jill are different but I don't have to define defaults (like two arms and a head) every time I define a new instance.

Recipe attributes:

default['human'][*] = { 
    "arms" => 2,
    "legs" => 2,
    "heads" => 1
}

In node/role:

default['human']['jack'] = { 
    "legs" => "1"
}
default['human']['jill'] = { 
    "superpower" => "flying"
}

Solution

  • Yes, chef attributes use deep merge and you can do this in the recipe, see http://docs.opscode.com/essentials_node_object_deep_merge.html

    Do this in the recipe:

    node['nginx']['sites'].each_key do |site|
      node.default['nginx']['sites'][site] = node['nginx']['site_defaults']
    end
    log JSON.pretty_generate(node['nginx']['sites'])
    

    Cookbook attributes:

    default['nginx']['site_defaults']['listen'] = [80]
    default['nginx']['site_defaults']['location'] = '/'
    default['nginx']['site_defaults']['index'] = ['index.html','index.htm']
    default['nginx']['sites']['api']['index'] = 'api.cgi'
    

    In node/role:

    "nginx" : {
      "sites" : {
        "blog" : {
          "location" : "/blog/",
          "listen" : [443]
        },
        "wiki" : {
          "index" : ["index.php"] 
        }
      }
    },
    

    Produces

      * log[{
      "api": {
        "listen": [
          80
        ],
        "location": "/",
        "index": [
          "index.html",
          "index.htm"
        ]
      },
      "blog": {
        "listen": [
          443
        ],
        "location": "/blog/",
        "index": [
          "index.html",
          "index.htm"
        ]
      },
      "wiki": {
        "listen": [
          80
        ],
        "location": "/",
        "index": [
          "index.php"
        ]
      }
    }] action write
    

    Note that if you do this, the recipe default has higher precedence over the attributes default so the api index was overridden. The following would work.

    force_default['nginx']['sites']['api']['index'] = 'api.cgi'
    

    Also be very careful of Arrays, sometimes they are merged other times replaced (above).