Search code examples
ansiblejinja2

How to split a string into a list with Ansible/Jinja2?


I have the following variables:

domain_names:
  - app1.example.com
  - app2.example.com

customers:
  - name: customer1
  - name: customer2

And I'm trying to generate the following list of domain names:

- customer1.app1.example.com
- customer2.app1.example.com
- customer1.app2.example.com
- customer2.app2.example.com

By using the following Ansible/Jinja2 code:

- name: check which certificates exist
  stat:
    path: '/etc/nginx/{{item}}.crt'
  register: cert_file
  loop: '{% for d in domain_names %}{{ d }} {% for customer in customers %}{{ customer.name }}.{{ d }} {% endfor %}{% endfor %}'

However, I'm getting the following error:

failed | msg: Invalid data passed to 'loop', it requires a list, got this instead: customer1.app1.example.com customer2.app1.example.com customer1.app2.example.com customer2.app2.example.com. Hint: If you passed a list/dict of just one element, try adding wantlist=True to your lookup invocation or use q/query instead of lookup.

How can I fix this?


Solution

  • Simply use the right tools :). In this case your friends are:

    e.g. test.yml playbook:

    ---
    - name: product and map filters demo
      hosts: localhost
      gather_facts: false
      
      vars:
        domain_names:
          - app1.example.com
          - app2.example.com
    
        customers:
          - name: customer1
          - name: customer2
    
      tasks:
        - name: Demonstrate product and map filters use
          debug:
            msg: "{{ item.0 }}.{{ item.1 }}"
          loop: "{{ customers | map(attribute='name') | product(domain_names) | list }}"
    

    Which gives:

    $ ansible-playbook test.yml 
    
    PLAY [product and map filters demo] *******************************************************************************************************************************************************************************
    
    TASK [Demonstrate product and map filters use] ********************************************************************************************************************************************************************
    ok: [localhost] => (item=['customer1', 'app1.example.com']) => {
        "msg": "customer1.app1.example.com"
    }
    ok: [localhost] => (item=['customer1', 'app2.example.com']) => {
        "msg": "customer1.app2.example.com"
    }
    ok: [localhost] => (item=['customer2', 'app1.example.com']) => {
        "msg": "customer2.app1.example.com"
    }
    ok: [localhost] => (item=['customer2', 'app2.example.com']) => {
        "msg": "customer2.app2.example.com"
    }
    
    PLAY RECAP ********************************************************************************************************************************************************************************************************
    localhost                  : ok=1    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
    
    

    Applied to your task, this gives:

    - name: Check which certificates exist
      stat:
        path: '/etc/nginx/{{ item.0 }}.{{ item.1 }}.crt'
      register: cert_file
      loop: "{{ customers | map(attribute='name') | product(domain_names) | list }}"
    

    If you really want to re-use that list you can build it into an other var. One easy way to understand is to build it in a set_fact task, e.g.

    - name: Create my application names list
      vars:
        current_name: "{{ item.0 }}.{{ item.1 }}"
      set_fact:
        application_names_list: "{{ application_names_list | default([]) + [current_name] }}"
      loop: "{{ customers | map(attribute='name') | product(domain_names) | list }}"
    
    - name: Check which certificates exist
      stat:
        path: '/etc/nginx/{{ item }}.crt'
      register: cert_file
      loop: "{{ application_names_list }}"
    

    But you can also declare it "statically" in your vars with a little more complex expression (see the map filter possibilities and the join filter)

    ---
    - name: product and map filters demo
      hosts: localhost
      gather_facts: false
    
      vars:
        domain_names:
          - app1.example.com
          - app2.example.com
    
        customers:
          - name: customer1
          - name: customer2
    
        application_names_list: "{{ customers | map(attribute='name') | product(domain_names) | map('join', '.') | list }}"
    
      tasks:
        - name: Demonstrate product and map filters use
          debug:
            var: application_names_list
    

    =>

    PLAY [product and map filters demo] ****************************************************************************************************************************************************************************************************
    
    TASK [Demonstrate product and map filters use] *****************************************************************************************************************************************************************************************
    ok: [localhost] => {
        "all_domains": [
            "customer1.app1.example.com",
            "customer1.app2.example.com",
            "customer2.app1.example.com",
            "customer2.app2.example.com"
        ]
    }
    
    PLAY RECAP *****************************************************************************************************************************************************************************************************************************
    localhost                  : ok=1    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0