Search code examples
ansiblejinja2

Using a list based fact inside a template produces error: `mapping values are not allowed in this context in "<unicode string>"`


I'm using the following fact:

- name: Set server ips fact
  ansible.builtin.set_fact:
    k3s_server_ips: "{{ k3s_server_hosts | map('extract', hostvars, ['ansible_default_ipv4', 'address']) }}"
  when: ansible_host in k3s_server_hosts

This produces:

ok: [apollo] => changed=false
  ansible_facts:
    k3s_server_ips:
    - 192.168.4.2
    - 192.168.4.3
    - 192.168.4.4

When I use the defined fact inside a Jinja2 Template:

kubeProxy:
  endpoints: '{{ k3s_server_ips }}'

The following error is displayed:

Traceback (most recent call last):
  File "/usr/local/Cellar/ansible/9.4.0/libexec/lib/python3.12/site-packages/ansible/executor/task_executor.py", line 526, in _execute
    self._task.post_validate(templar=templar)
  File "/usr/local/Cellar/ansible/9.4.0/libexec/lib/python3.12/site-packages/ansible/playbook/task.py", line 290, in post_validate
    super(Task, self).post_validate(templar)
  File "/usr/local/Cellar/ansible/9.4.0/libexec/lib/python3.12/site-packages/ansible/playbook/base.py", line 543, in post_validate
    value = method(attribute, getattr(self, name), templar)
            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/Cellar/ansible/9.4.0/libexec/lib/python3.12/site-packages/ansible/playbook/task.py", line 298, in _post_validate_args
    args = templar.template(value)
           ^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/Cellar/ansible/9.4.0/libexec/lib/python3.12/site-packages/ansible/template/__init__.py", line 791, in template
    d[k] = self.template(
           ^^^^^^^^^^^^^^
  File "/usr/local/Cellar/ansible/9.4.0/libexec/lib/python3.12/site-packages/ansible/template/__init__.py", line 764, in template
    result = self.do_template(
             ^^^^^^^^^^^^^^^^^
  File "/usr/local/Cellar/ansible/9.4.0/libexec/lib/python3.12/site-packages/ansible/template/__init__.py", line 1010, in do_template
    res = myenv.concat(rf)
          ^^^^^^^^^^^^^^^^
  File "/usr/local/Cellar/ansible/9.4.0/libexec/lib/python3.12/site-packages/ansible/template/native_helpers.py", line 43, in ansible_eval_concat
    head = list(islice(nodes, 2))
           ^^^^^^^^^^^^^^^^^^^^^^
  File "<template>", line 18, in root
  File "/usr/local/Cellar/ansible/9.4.0/libexec/lib/python3.12/site-packages/ansible/template/__init__.py", line 295, in wrapper
    ret = func(*args, **kwargs)
          ^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/Cellar/ansible/9.4.0/libexec/lib/python3.12/site-packages/ansible/plugins/filter/core.py", line 221, in from_yaml
    return yaml_load(text_type(to_text(data, errors='surrogate_or_strict')))
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/Cellar/ansible/9.4.0/libexec/lib/python3.12/site-packages/yaml/__init__.py", line 81, in load
    return loader.get_single_data()
           ^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/Cellar/ansible/9.4.0/libexec/lib/python3.12/site-packages/yaml/constructor.py", line 49, in get_single_data
    node = self.get_single_node()
           ^^^^^^^^^^^^^^^^^^^^^^
  File "yaml/_yaml.pyx", line 673, in yaml._yaml.CParser.get_single_node
  File "yaml/_yaml.pyx", line 687, in yaml._yaml.CParser._compose_document
  File "yaml/_yaml.pyx", line 731, in yaml._yaml.CParser._compose_node
  File "yaml/_yaml.pyx", line 845, in yaml._yaml.CParser._compose_mapping_node
  File "yaml/_yaml.pyx", line 731, in yaml._yaml.CParser._compose_node
  File "yaml/_yaml.pyx", line 847, in yaml._yaml.CParser._compose_mapping_node
  File "yaml/_yaml.pyx", line 860, in yaml._yaml.CParser._parse_next_event
yaml.scanner.ScannerError: mapping values are not allowed in this context
  in "<unicode string>", line 26, column 68
fatal: [apollo]: FAILED! => changed=false

This happens only when I use the fact inside the template, if I use k3s_server_ips inside a task, there are no issues.

As workaround, I tried:

kubeProxy:
  endpoints:
{% for ip in k3s_server_ips %}
    - {{ ip }}
{% endfor %}

Which fixes the issue. I'm wondering if there is a better implementation.


Solution

  • The error message

    yaml.scanner.ScannerError: mapping values are not allowed in this contextvin "<unicode string>"
    

    indicates syntax and or indention errors within your created YAML structure.

    For a Jinja2 Template called templates/my_list.j2 as input and with content of

    kubeProxy:
      endpoints:
        {{ k3s_server_ips | to_nice_yaml(indent=4) | trim | indent(4) }}
      as_well:
    {% for ip in k3s_server_ips %}
        - {{ ip }}
    {% endfor %}
    

    processing with a minimal example playbook

    ---
    - hosts: localhost
      become: false
      gather_facts: false
    
      vars:
    
        k3s_server_ips:
          - 192.168.4.2
          - 192.168.4.3
          - 192.168.4.4
    
      tasks:
    
      - template:
          src: templates/my_list.j2
          dest: kubeProxy.endpoints.yml
    
      - name: "Set from template"
        set_fact:
          my_list: "{{ lookup('ansible.builtin.template', 'templates/my_list.j2') }}"
    
      - debug:
          msg: "{{ my_list | from_yaml }}"
    

    will result into an output of

    TASK [debug] ******
    ok: [localhost] =>
      msg:
        kubeProxy:
          as_well:
          - 192.168.4.2
          - 192.168.4.3
          - 192.168.4.4
          endpoints:
          - 192.168.4.2
          - 192.168.4.3
          - 192.168.4.4
    

    as well a file written, called kubeProxy.endpoints.yml with content of

    kubeProxy:
      endpoints:
        - 192.168.4.2
        - 192.168.4.3
        - 192.168.4.4
      as_well:
        - 192.168.4.2
        - 192.168.4.3
        - 192.168.4.4
    

    Thanks To / Similar Q&A


    "I'm wondering if there is a better implementation."

    Probably yes, by not using set_fact, respective avoiding intermediate facts. Just take the shortcut and not the detour via the playbook by directly generating the k3s_server_ips list within the Jinja2 Template.