Search code examples
jinja2markdowntemplatingjinja2-cli

Variable usage in jinja2 inside markdown template


I am trying to create a template of a markdown file using jinja and the values of the variables are stocked in .yml file ( kind of an inventory of hosts).

my problem is that I think that markdown table that I am trying to fill are not making it easy and since I have tried many alternatives using jinja2 tools and function and still no success I am adressing this issue to the community in hope of getting some insight or tips to get over the problem:

my markdown file contains table such as :

## Servers

### Cluster 1
|       | IP | FQDN |
|-------|----|------|
|       |    |      |

my value file .yml is as follows :

servers:
  clusters:
    - id: 1
      test: X.X.X.X
      nodes:
        - X.X.X.X
        - X.X.X.X
        - X.X.X.X

in order to retrieve the right values to fill the table I wrote this :

{% set id = 1 %}
|       | IP | FQDN |
|-------|----|------|
|  test  | {{servers.clusters.id.test}} |      |
{% for node in servers.clusters.id.nodes %}|node{{node.id}}|{{node.ip}}|{{node.fqdn}}|
{% endfor %} 

but it doesn't seem to work and the error is not very explicit (to a jinja2 beginner of course) :

File "[PATH]/filename.md", line 34, in top-level template code
    |  test  | {{server.clusters.id.test}} |      |
  File "/usr/lib/python3.8/site-packages/jinja2/environment.py", line 471, in getattr
    return getattr(obj, attribute)
jinja2.exceptions.UndefinedError: 'list object' has no attribute 'id'

All suggestions are welcome.


Solution

  • You need to loop through the items contained in clusters or reference it by index as it is a list.

    The trick is to understand the structure of the data returned by the YAML parser and how to access that structure from within Jinja.

    Jinja expressions are mostly just Python code with a few minor differences. For example, Jinja provides a shortcut which allows you to access dicts using dot syntax. Normally, in Python one would do mydict['keyname'] to retrieve a value. However, Jinja also supports doing mydict.keyname Under the hood it actually calls mydict.keyname but when that fails, it tries mydict['keyname'] as a fallback. If both fail, it raises the first error, which is what you are seeing ('list object' has no attribute 'id').

    Note that the clusters item contains a list (as indicated by the -), which is why the error refers to a 'list object'. You cannot access items of a list using either mylist.keyname or mylist['keyname']. You either need to loop through the list or reference a specific item by its index by number (mylist[0] for the first item). Unless you are certain that the list will never contain more than one item (in which case why is it a list?) then you will likely want to loop through the items.

    It appears that you are trying to only include the data for the single item with id is 1. If that will always be the first item in the list, you could do: servers.clusters.[0].test. However, if you can't be sure of that, then you would need to loop through all items and wrap the actual statement within an if statement which checks that the id is equal to the previous set variable id.

    Like this for the IP column of the first row:

    {% for cluster in servers.clusters %}{% if cluster.id == id %}{{ cluster.test }}{% endif %}{% endfor %}
    

    Note that cluster.id makes reference to the key id of an item in clusters. It is not the id set by {% set id = 1 %}. However, it can be compared to it and if the two are equal (both contain the value 1 in this case), then the expression is True and the expression contained within is executed. When the two are not equal, then it is ignored.

    You have the same issue for the second row. However, nodes also contains a list of strings. There are no attributes on any of those items so you would just render node in the second loop (rather than node.id, node.ip and node.fqdn). Therefore, I assume you want something like this:

    {% for cluster in servers.clusters %}{% if cluster.id == id %}|{% for node in cluster.nodes %} {{ node }} |{% endfor %}{% endif %}{% endfor %}
    

    Of course, you could combine that all together and only do the loop once:

    {% set id = 1 %}
    |       | IP | FQDN |
    |-------|----|------|
    {% for cluster in servers.clusters %}{% if cluster.id == id %}| test | {{ cluster.test }} |      |
    |{% for node in cluster.nodes %} {{ node }} |{% endfor %}
    {% endif %}
    {% endfor %}
    

    Naturally, if you wanted to combine all clusters in a single table, you would remove the if check and have a single row for each cluster. But that would be a different table that the one you have asked for.

    If you wanted a separate table for each cluster, you have a few options. You could use the same approach and simply redefine the id variable. Like this:

    ### Cluster 1
    {% set id = 1 %}
    |       | IP | FQDN |
    |-------|----|------|
    {% for cluster in servers.clusters %}{% if cluster.id == id %}| test | {{ cluster.test }} |      |
    |{% for node in cluster.nodes %} {{ node }} |{% endfor %}
    {% endif %}
    {% endfor %}
    
    
    ### Cluster 2
    {% set id = 2 %}
    |       | IP | FQDN |
    |-------|----|------|
    {% for cluster in servers.clusters %}{% if cluster.id == id %}| test | {{ cluster.test }} |      |
    |{% for node in cluster.nodes %} {{ node }} |{% endfor %}
    {% endif %}
    {% endfor %}
    

    Note that the only difference between the two are the first two lines:

    ### Cluster 2
    {% set id = 2 %}
    

    Everything else is the same. But if you are just repeating the same code, that is not very efficient. Any future changes would need to be made for each cluster. Instead, just wrap the whole thing up in a loop:

    {% for cluster in servers.clusters %}
    ### Cluster {{ cluster.id }}
    
    |       | IP | FQDN |
    |-------|----|------|
    | test | {{ cluster.test }} |      |
    |{% for node in cluster.nodes %} {{ node }} |{% endfor %}
    {% endfor %}
    

    Notice that the loop wraps everything, including the header. The header then gets the cluster.id. And, as the body of the table will be repeated for each cluster, we do not need the if statement confining it to only one cluster.

    It is important to note that this approach will only work if the data for each and every cluster is in the same format/structure.