Search code examples
ansiblejinja2

Ansible templating with special characters


I have the following Ansible variable:

environment:
  CONFIG: |
    external_url 'https://www.example.com'
    external_port '9090'
  USERNAME: root
  ROOT_PASSWORD: verysecret

Ansible template file:

environment:
{% for key, value in environment.items() %}
{{ key }}: {{ value }}
{% endfor %}

The output should be:

environment:
  CONFIG: |
    external_url 'https://www.example.com'
    external_port '9090'
  USERNAME: root
  ROOT_PASSWORD: verysecret

But the output is:

environment:
  CONFIG: external_url 'https://www.example.com'
external_port '9090'

  USERNAME: root
  ROOT_PASSWORD: verysecret

It seems the | character is not literally used. It also breaks the indentation. Which filter should I use in my template?

Added my playbook and more since it was asked for:

template.j2:

---
- hosts: localhost
  vars:
    docker_compose_projects:
       - name: project_1
         services:
           - service_name: some_service
             image: some_image:latest
             environment:
               CONFIG: |
                 external_url 'https://www.example.com'
                 external_port '9090'
               USERNAME: root
               ROOT_PASSWORD: verysecret
  tasks:
    - name: template
      ansible.builtin.template:
        src: template.yml.j2
        dest: "/home/username/{{ item.name }}"
      loop: "{{ docker_compose_projects }}"

template.yml.j2:

services:
{% for service in item.services %}
  {{ service.service_name }}
    image: {{ service.image }}
{% if service.environment is defined %}
  environment:
{% for key, value in service.environment.items() %}
    {{ key }}: {{ value }}
{% endfor %}
{% endif %}
{% endfor %}

Output (project_1):

services:
  some_service:
    image: some_image:latest
    environment:
      CONFIG: external_url 'https://www.example.com'
external_port '9090'

      USERNAME: root
      ROOT_PASSWORD: verysecret

Expected output:

services:
  some_service:
    image: some_image:latest
    environment:
      CONFIG: |
        external_url 'https://www.example.com'
        external_port '9090'
      USERNAME: root
      ROOT_PASSWORD: verysecret

Solution

  • A bit of explanation

    You need to put | to the template itself to keep the multiline formatting, and render the lines of multiline strings in a nested loop:

    {{ key }}: |
      {% for line in value.splitlines() %}
      {{ line }}
      {% endfor %}
    

    But since your variables could be both single-line and multiline, you will have to keep a default option, too:

        {% for key, value in service.environment.items() %}
        {% if '\n' in value %}
          {{ key }}: |
            {% for line in value.splitlines() %}
            {{ line }}
            {% endfor %}
        {% else %}
          {{ key }}: "{{ value }}"
        {% endif %}
        {% endfor %}
    

    You will also have to manually control the whitespaces. Given that your template already has different indents for keys of the same level, it will be hard to achieve that individually for each block. To keep the template readable and editable, you can set #jinja2: lstrip_blocks:True, trim_blocks:True globally, and maintain the desired indentation within the template directly (see the next chapter).

    Correct template

    It probably could be optimized and beautified further, but I think it's enough to show the idea. Note that I also fixed some syntax problems of your original template such as missed : after {{ service.service_name }} and unquoted image name:

    #jinja2: lstrip_blocks:True, trim_blocks:True
    ---
    services:
      {% for service in item.services %}
      {{ service.service_name }}:
        image: "{{ service.image }}"
      {% if service.environment is defined %}
        environment:
        {% for key, value in service.environment.items() %}
        {% if '\n' in value %}
          {{ key }}: |
            {% for line in value.splitlines() %}
            {{ line }}
            {% endfor %}
        {% else %}
          {{ key }}: "{{ value }}"
        {% endif %}
        {% endfor %}
      {% endif %}
      {% endfor %}
    

    Example

    Let's take this simplified playbook as a minimal reproducible example (of course, the variables should reside in group or host vars, and the secrets should be encrypted or injected):

    ---
    - hosts: localhost
      gather_facts: false
      vars:
        docker_compose_projects:
          - name: project_1
            services:
              - service_name: some_service_1
                image: "some_image_1:latest"
                environment:
                  CONFIG: |
                    external_url 'https://www.example.com'
                    external_port '9090'
                  USERNAME: root
                  ROOT_PASSWORD: verysecret1
              - service_name: some_service_2
                image: "some_image_2:latest"
                environment:
                  USERNAME: root
                  ROOT_PASSWORD: verysecret2
              - service_name: some_service_3
                image: "some_image_3:latest"
      tasks:
        - name: Render the template
          template:
            src: template-trimmed.yml.j2
            dest: "target_dir/{{ item.name }}.yml"
          loop: "{{ docker_compose_projects }}"
    

    The template from the previous chapter will generate this beatiful YAML file:

    ---
    services:
      some_service_1:
        image: "some_image_1:latest"
        environment:
          CONFIG: |
            external_url 'https://www.example.com'
            external_port '9090'
          USERNAME: "root"
          ROOT_PASSWORD: "verysecret1"
      some_service_2:
        image: "some_image_2:latest"
        environment:
          USERNAME: "root"
          ROOT_PASSWORD: "verysecret2"
      some_service_3:
        image: "some_image_3:latest"