Steve K
Steve K

Reputation: 387

Using attribute values in other attributes within the same attributes file in chef

Note: My example here is meant to display the context of my question, not a real-world example.

I have the following as an example in my attributes/default.rb file:

default[:php] = {
    version: '7.3',
    repository: 'ondrej/php',
    extensions: [
        "php7.3",
        "php7.3-common",
        "php7.3-gd",
        "php7.3-mysql",
        "php7.3-curl",
        "php7.3-intl",
        "php7.3-xsl",
        "php7.3-mbstring",
        "php7.3-zip",
        "php7.3-bcmath",
        "php7.3-iconv",
        "php7.3-soap",
        "php7.3-fpm",
        "libapache2-mod-php7.3"
    ],
    options: {
        :timezone => 'America/Los Angeles',
        :memory_limit => '2G',
        :max_execution_time => 1800,
        :zlib_output_compression => 'On',
        :session_save_path => '/var/lib/sessions'
    }
}

Next, I pass that to a template like so:

template '/home/vagrant/php_test.txt' do
    source node['url'] % {version: node['version']}
    source 'php_test.txt.erb'
    owner 'vagrant'
    group 'vagrant'
    mode '0755'
    variables(
        php: node[:php]
    )
end

And then I reference the values in my template like this:

PHP Version : <%= @php[:version] %>
PHP Repository : <%= @php[:repository] %>

PHP Extension List:
<% @php[:extensions].each do |extension| %>
<%= "#{extension}" %>
<% end %>

PHP Options List
<% @php[:options].each do |option, value| %>
<%= "#{option}: #{value}" %>
<% end %>

This results in the following text file:

PHP Version : 7.3
PHP Repository : ondrej/php

PHP Extension List:
php7.3
libapache2-mod-php7.3
php7.3-common
php7.3-gd
php7.3-mysql
php7.3-curl
php7.3-intl
php7.3-xsl
php7.3-mbstring
php7.3-zip
php7.3-bcmath
php7.3-iconv
php7.3-soap
php7.3-fpm

PHP Options List
timezone: America/Los Angeles
memory_limit: 2G
max_execution_time: 1800
zlib_output_compression: On
session_save_path: /var/lib/sessions

Everything works fine; however, the DRY-ist in me sees all of those 7.3's in there and says, "Hey, why don't you just reference the value of version within the value of each extension in the list?" So, I try this in my attributes/default.rb file:

...

extensions: [
    "php#{node[:php][:version]}",
    "php#{node[:php][:version]}-common",
    "php#{node[:php][:version]}-gd",
    "php#{node[:php][:version]}-mysql",
    "php#{node[:php][:version]}-curl",
    "php#{node[:php][:version]}-intl",
    "php#{node[:php][:version]}-xsl",
    "php#{node[:php][:version]}-mbstring",
    "php#{node[:php][:version]}-zip",
    "php#{node[:php][:version]}-bcmath",
    "php#{node[:php][:version]}-iconv",
    "php#{node[:php][:version]}-soap",
    "php#{node[:php][:version]}-fpm",
    "libapache2-mod-#{node[:php][:version]}"
]

...

There are (at least) two problems with this. First, it doesn't do what I expect. When I loop over the array in my template, each of the values are printed as strings rather than interpolated. Second, it breaks chef best practice as mentioned here: https://github.com/pulseenergy/chef-style-guide#attributes

So then, what's the best way to use the value of one attribute in the population of another?

In thinking about this, there are two options I can think of:

  1. Use delayed interpolation as indicated here: https://coderanger.net/derived-attributes/

The issue I have with this is that I don't quite understand how the suggested concept would work as my ruby skills aren't quite up to par.

  1. Store the extensions without the php- suffix and then loop over them in a recipe and concatenate php, the version string, and the extensions into a single value per extension. Store each of those in a new array which gets passed through to the template.

Is there another way to achieve this; possibly by forcing the "php{node[:php][:version]}" attributes to interpolate in the template?

Upvotes: 0

Views: 389

Answers (1)

Chris Heald
Chris Heald

Reputation: 62698

I don't know Chef, but reading that best practices page, derived attributes via delayed interpolation is the right approach here. It should be fairly straightforward:

extensions: [
  "php%{version}",
  "php%{version}-common",
  "php%{version}-gd",
  # ...
]

Then later, when you render it:

<% @php[:extensions].each do |extension| %>
<%= extension % @php %>
<% end %>

This is a little unobvious, but it works because string % hash will use the keys in hash to replace any named placeholders in the string. Hash values that don't exist as named placeholders are ignored.

"test: %{bang}" % {baz: "bin", bang: "bar"}
=> "test: bar"

Since your @php variable has a :version key, it will be substituted into the value of each extension string as it's rendered, if that string contains the placeholder %{version}.

You could do it more explicitly, as well:

# Create a hash with the one key:
<%= extension % {version: @php[:version]} %>

# Or, just create a hash by slicing out just the attribute you care about
<%= extension % @php.slice(:version) %>

Upvotes: 1

Related Questions