Search code examples
rubyif-statementtemplatesiterationpuppet

Nested iteration in Puppet template [Ruby]


I started to learn Puppet this week and trying hard to implement user's keys to /etc/ssh/sudo_authorized_keys.

I have a dictionary of users with keys in sudo_users.yaml:

core::sudo_users_keys:
  kate:
    keys:
      key1:
        type: "ssh-ed25519"
        key: "AAAAC3N..."
  john:
    keys:
      key1:
        type: "ssh-ed25519"
        key: "AAAAC..."
  marvin:
    keys:
      key1:
        type: "ssh-ed25519"
        key: "AAAAC3Nza..."

Then I create this in sudokeys.pp file:

class core::sudokeys {
    file { "/etc/ssh/sudo_authorized_keys":
      ensure  => file,
      mode    => "0440",
      content => template("core/sudo_authorized_keys.erb"),
    }

As you can see, I want to implement template with iteration. This is my current template:

<%- scope['core::sudo_users_keys'].each | user | -%>
    {
        <%- if user[1] -%>
        <%- $user[1]['keys'].each do | key | -%>
        <%= $key[1]['type'] $key[1]['key'] -%> 
        <%- end end -%>
      }
    <%- end -%>

I have the same dictionary with id_rsa keys to ssh and use interaction as below. It works perfectly for ssh_authorized_key, but I can use it in this case by adding keys to /etc/ssh/sudo_authorized_keys. That's why I decided to use a template and only inject keys inside the sudo_authorized_keys file.

class core::sshkeys {
  lookup("core::sudo_users_keys").each | $user | {
    if $user[1] {
      $user[1]["keys"].each | $key | {
        ssh_authorized_key { "${user[0]}-${key[0]}":
          ensure => present,
          user   => $user[0],
          type   => $key[1]["type"],
          key    => $key[1]["key"],
        }
      }
    }
  }
}

Puppet documentation doesn't include this kind of more complicated iterations and I feel like wandering in the fog.

Currently, I'm getting this error when deploying my template, but I feel like the way I prepare this will not work as I wanted.

Internal Server Error: org.jruby.exceptions.SyntaxError: (SyntaxError) /etc/puppetlabs/code/environments/test/modules/core/templates/sudo_authorized_keys.erb:2: syntax error, unexpected tSTRING_BEG
_erbout.<< "    {\n".freeze
           ^

I will appreciate any suggestions about template construction. What should I change to make it work and extract only type and key values?


Solution

  • Your approach is workable, but there are multiple issues with your code, among them

    • the scope object in a template provides access to Puppet variables, not to Hiera data. Probably the easiest way to address that would be to

      1. give your core::sudokeys class a class parameter to associate with the data ($userkeys, say),
      2. change the Hiera key for the data so that it will be automatically bound to the new class parameter, and
      3. in the template, access the data via the class parameter (which doesn't even require using the scope object in that case).
    • in the template, you seem to be assuming a different structure for your data than it actually has in Hiera. Your data is a hashes of hashes of hashes, but you are accessing parts of it as if there were arrays involved somewhere (user[1], key[1]). Also, if there were one or more arrays, then do be aware that Ruby array indexes start at 0, not at 1.

    • Scriptlet code in an ERB template is (only) Ruby. Your scriptlet code appears to mix Ruby and Puppet syntax. In particular, Ruby variable names are not prefixed with a $, and Ruby block parameters appear inside the block, not outside it.

    • <%= ... %> tags a for outputting the value of one Ruby expression each. You appear to be trying to emit multiple expressions with the same tag.

    Also, it's worth noting that scriptlet code can span multiple lines. That can make it clearer. Additionally, you may need to pay attention to whitespace and newlines in your template to get the output you want. ERB has options to consume unwanted whitespace around scriptlet tags, if needed, but sometimes its at least as easy to just avoid putting in whitespace that you don't want. On the other hand, be sure not to suppress whitespace that you actually wanted.

    So, here's one form that all of the above might take:

    sudo_users.yaml

    core::sudokeys::users_keys:
      kate:
        keys:
          key1:
            type: "ssh-ed25519"
            key: "AAAAC3N..."
      john:
        keys:
          key1:
            type: "ssh-ed25519"
            key: "AAAAC..."
      marvin:
        keys:
          key1:
            type: "ssh-ed25519"
            key: "AAAAC3Nza..."
    

    sudokeys.pp

    class core::sudokeys($users_keys) {
        file { "/etc/ssh/sudo_authorized_keys":
          ensure  => file,
          mode    => "0440",
          content => template("core/sudo_authorized_keys.erb"),
        }
    }
    

    sudo_authorized_keys.erb

    <% @users_keys.each do | user |
          if user.has_key?('keys') do
            user['keys'].each do |key|
              %><%= key['type'] %> <%= key['key'] %>
    <%        # don't consume preceding whitespace here
            end
          end
        end
    %>