Default values for data bag attributes


#1

Hi.

I’m writing a recipe for chef-zero 12.3.0. I’d like to use attributes stored in data bags, but if the data bag item doesn’t define the attribute, a default defined as an attribute in the cookbook should be used.

What’s the best way to do this? I now have:

ipa_node = data_bag_item("ipa", node['hostname'])
ipaddress = (ipa_node['ipaddress'].empty?) ? (default[:ipa][:server][:ipaddress]) : (ipa_node['ipaddress'])

Or even:

values = {}
default[:ipa][:server].each do |key, default_val|
    values[key.to_s] = (ipa_node[key].empty?) ? (default_val) : (ipa_node[key])
end

This seems to be a bit „awkward“, especially if numerous attributes from a data bag item are to be used.

How do I use values from data bags with a fallback to defaults in a „canonical” way?

Thanks a lot,
Alexander


#2

There is no “standard” way to do it but here is how I solved this:

Create a library in some base cookbook with a code like this (I just copy/pasted some code here, so this might not work out of the box but the concepts should be there):

module MySettings
  # Loads settings from node attributes and data_bags/vaults.
  #
  # @param [String] The app to load. App is the key in data_bags/my_settings/<NODE_NAME>.json
  def app_settings_for(app)
      # Return 'chached' settings from run_state
      settings_name = "MY_SETTINGS_FOR_#{app}"
      return node.run_state[settings_name] unless node.run_state[settings_name].nil?

      # No cached run_state, thus get settings from vault/data_bag

      # Create a copy of node attributes to be merged with vault settings
      # If node attribute does not exist use empty hash
      # In ruby dup should do the same, but we want a real hash and not a node object
      settings = node["company-#{app}"].to_hash if node.key? "company-#{app}"
      settings ||= {}

      node_vault = load_vault_if_exists('bag_name', node.name)
      # hash_only_merge!(a, b) will merge b into a _AND_ overwrite a
      Chef::Mixin::DeepMerge.hash_only_merge!(settings, node_vault[app]) if node_vault.key? app

      # Set run_state 'cache' and return settings
      node.run_state[settings_name] = settings

      settings
  end

  def load_vault_if_exists(bag, name)
      begin
        s = chef_vault_item(bag, name).raw_data if Chef::DataBag.load(bag).include?(name)
      rescue ChefVault::Exceptions::KeysNotFound, ChefVault::Exceptions::SecretDecryption
        raise "This node does not have access to vault item #{app}"
      end
      s ||= {} # Return empty hash if item does not exist

      # Cache vault data in run_state
      node.run_state[vault_settings_name] = s

      s
  end
end

Chef::Recipe.send(:include, MySettings)
Chef::Resource.send(:include, MySettings)

In my recipes where I use these settings I always have this in one of the first lines:

settings = app_settings_for('jenkins')

template '/watever.json' do
  variables(
    example: settings['merged']
  )
end

This allows us to place the merge logic and general settings usage in one place. If we decide to switch from DataBags to some Database or Hashicorp Vault this could relatively easily be done in the one single library.