How to dynamically feed hash array object to Chef ERB template resource

Viewed 69

Trying to configure multiple yum repos using an array as the source in Chef using the Template resource. Some repos have includpkgs or exclude parameters. Some are targeting mirrors most target a single baseurl. In short, the repos definitions are not identical parameter-wise. Here's an example of the repo definitions in my Chef attributes file.

default['ecmc_yum_repo']['server']['repos']['chef-stable'] = {
  'baseurl': 'https://rpm.ecmc.lan/repos/chef-stable',
  'name': 'Chef Repo',
  'enabled': '0',
  'gpgcheck': '1',
  'gpgkey': 'file:///etc/pki/rpm-gpg/RPM-GPG-KEY-CHEF',
  'gpgkeysource': 'https://packages.chef.io/chef.asc',
  'gpgkeyversion': 'gpg-pubkey-83ef826a-4a690bb4',
  'includepkgs': 'chef-workstation',
}

default['yum_repo']['server']['repos']['epel'] = {
  'name': 'Extra Packages for Enterprise Linux 7 - $basearch',
  'enabled': '1',
  'gpgcheck': '1',
  'gpgkey': 'file:///etc/pki/rpm-gpg/RPM-GPG-KEY-EPEL-7',
  'gpgkeysource': 'https://dl.fedoraproject.org/pub/epel/RPM-GPG-KEY-EPEL-7',
  'gpgkeyversion': 'gpg-pubkey-352c64e5-52ae6884',
  'metalink': 'https://mirrors.fedoraproject.org/metalink?repo=epel-7&arch=x86_64',
}

default['yum_repo']['server']['repos']['teams'] = {
  'baseurl': 'https://packages.microsoft.com/yumrepos/ms-teams',
  'name': 'Microsoft Teams Repo',
  'enabled': '0',
  'exclude': 'teams-insiders*',
  'gpgcheck': '1',
  'gpgkey': 'file:///etc/pki/rpm-gpg/RPM-GPG-KEY-MICROSOFT',
  'gpgkeysource': 'https://packages.microsoft.com/keys/microsoft.asc',
  'gpgkeyversion': 'gpg-pubkey-be1229cf-5631588c',
}

In my Chef recipe, as I loop through the arrays, I would like to feed all the items in the array to the my template resource that will create the yum repo file EXCEPT for the gpgkeysource and gpgkeyversion. Keep in mind this is shortened view of what I'm doing in the recipe. I'm calling other resources besides template that use those array items. But for the repo file template resource, I want all but those two items. So far I've been unable to accomplish this in a dynamic fashion (as in feed the template everything but these two values). If all the repos had the exact same parameters it wouldn't be a problem,but they don't.

To start troubleshooting I'm simply trying to pass all the parameters from the arrays as follows:

node['yum_repo']['server']['repos'].each do |reponame, config|

  # Create repositories
  template "/etc/yum.repos.d/#{reponame}.repo" do
    source 'repofile.repo.erb'
    owner 'root'
    group 'root'
    mode '0644'
    variables(repoid: "#{reponame}",
              repo_config: "#{config}")
    action :create
  end
end

In my template file, I then attempt the following:

# This file was generated by Chef
# Do NOT modify this file by hand.
[<%= @repoid %>]
<%  @repo_config.each do |key, value| -%>
<%= key %>=<%= value %>
<% end -%>

This fails with:

Error executing action `create` on resource 'template[/etc/yum.repos.d/chef-stable.repo]'

Chef::Mixin::Template::TemplateError
------------------------------------
undefined method `each' for "{\"baseurl\"=>\"https://rpm.mydomain.lan/repos/chef-stable\", \"name\"=>\"Chef Repo\", \"enabled\"=>\"0\", \"gpgcheck\"=>\"1\", \"gpgkey\"=>\"file:///etc/pki/rpm-gpg/RPM-GPG-KEY-CHEF\", \"gpgkeysource\"=>\"https://packages.chef.io/chef.asc\", \"gpgkeyversion\"=>\"gpg-pubkey-83ef826a-4a690bb4\", \"includepkgs\"=>\"chef-workstation\"}":String

Farther down in the output I see:

           Compiled Resource:
           ------------------
           # Declared in /tmp/kitchen/cache/cookbooks/yum_repo/recipes/repo-non-amazon.rb:43:in `block in from_file'
           
           template("/etc/yum.repos.d/chef-stable.repo") do
             action [:create]
             default_guard_interpreter :default
             source "repofile.repo.erb"
             declared_type :template
             cookbook_name "yum_repo"
             recipe_name "repo-non-amazon"
             owner "root"
             group "root"
             mode "0644"
             variables {:repoid=>"chef-stable", :repo_config=>"{\"baseurl\"=>\"https://rpm.mydomain.lan/repos/chef-stable\", \"name\"=>\"Chef Repo\", \"enabled\"=>\"0\", \"gpgcheck\"=>\"1\", \"gpgkey\"=>\"file:///etc/pki/rpm-gpg/RPM-GPG-KEY-CHEF\", \"gpgkeysource\"=>\"https://packages.chef.io/chef.asc\", \"gpgkeyversion\"=>\"gpg-pubkey-83ef826a-4a690bb4\", \"includepkgs\"=>\"chef-workstation\"}"}
             path "/etc/yum.repos.d/chef-stable.repo"
           end
           
           Template Context:
           -----------------
           on line #4
             2: # Do NOT modify this file by hand.
             3: [<%= @repoid %>]
             4: <%  @repo_config.each do |key, value| -%>
             5: <%= key %>=<%= value %>
             6: <% end -%>
           
           System Info:
           ------------
           chef_version=17.1.35
           platform=amazon
           platform_version=2
           ruby=ruby 3.0.1p64 (2021-04-05 revision 0fb782ee38) [x86_64-linux]
           program_name=/opt/chef/bin/chef-client
           executable=/opt/chef/bin/chef-client
           
       
       Running handlers:
       [2022-05-24T14:57:42-05:00] ERROR: Running exception handlers
       Running handlers complete
       [2022-05-24T14:57:42-05:00] ERROR: Exception handlers complete
       Chef Infra Client failed. 0 resources updated in 06 seconds
       [2022-05-24T14:57:42-05:00] FATAL: Stacktrace dumped to /tmp/kitchen/cache/chef-stacktrace.out
       [2022-05-24T14:57:42-05:00] FATAL: Please provide the contents of the stacktrace.out file if you file a bug report
       [2022-05-24T14:57:42-05:00] FATAL: Chef::Mixin::Template::TemplateError: undefined method `each' for "{\"baseurl\"=>\"https://rpm.mydomain.lan/repos/chef-stable\", \"name\"=>\"Chef Repo\", \"enabled\"=>\"0\", \"gpgcheck\"=>\"1\", \"gpgkey\"=>\"file:///etc/pki/rpm-gpg/RPM-GPG-KEY-CHEF\", \"gpgkeysource\"=>\"https://packages.chef.io/chef.asc\", \"gpgkeyversion\"=>\"gpg-pubkey-83ef826a-4a690bb4\", \"includepkgs\"=>\"chef-workstation\"}":String

So it seems Chef is not able to iterate through the repo_config variable that is fed to the template. Apparently is being seen as a String object??

Can this even be accomplished? None of my searches has yielded a solution and my limited brain is fried by now.

1 Answers

This seems to be due to the use of "#{...}" during the variable assignment for template. When I tried with your code, I get the error:

undefined method `each' for #<String:0x0000000004ed6238>

From your error also, we can see that the repo_config is a string. Note the "{ in the message.

"{\"baseurl\"=>\"https://rpm.mydomain.lan/repos/chef-stable\", ...

Whereas a Hash would look like:

{"baseurl"=>"https://rpm.ecmc.lan/repos/chef-stable", ...

The use of "#{...}" is not required for variable assignment. Variable assigned as below seems to have coerced the hash into a string, and that's why the .each method doesn't work.

    variables(repoid: "#{reponame}",
              repo_config: "#{config}")

With this in mind, the below resource declaration should work fine:

node['yum_repo']['server']['repos'].each do |reponame, config|

  # Create repositories
  template "/etc/yum.repos.d/#{reponame}.repo" do
    source 'repofile.repo.erb'
    owner 'root'
    group 'root'
    mode '0644'
    variables(repoid: reponame,
              repo_config: config)
    action :create
  end
end

You could even consider using the yum_repository resource. It can manage your YUM repositories without having to write templates. A simple example, without all of the properties:

node['yum_repo']['server']['repos'].each do |reponame, repoconfig|
  yum_repository reponame do
    baseurl repoconfig['baseurl']
    description repoconfig['name']
    enabled repoconfig['enabled']
    metalink repoconfig['metalink']
    action :create
  end
end

Only consideration is that the attributes need to be adjusted for booleans such as:

  'name': 'Extra Packages for Enterprise Linux 7 - $basearch',
  'enabled': true,
  'gpgcheck': true,
Related