ansibleconfiguration-management

How to extend dict in ansible, not overwriting it?


Let's say I have an ansible role which being imported in any place will clone repositories to specified path, called "git_role", for example.

It's mechanism, clone repositories for projects doing by one role when it's inlcuded to playbook.

And it's doing by assigning dict like

repos:
  name_of_repo:
    url: "git@myrepo.com"
    path: "/path/on/local_system"
    branch: "branch_to_checkout"

And this dict on "git_role" which doing clone repo handled like this:

- name: Clone repositories
  git:
    repo: "{{ item['value']['url'] }}"
    dest: "{{ item['value']['path'] }}"
    accept_hostkey: yes
  loop: "{{ repos | dict2items }}"

I want that "repos" dict can be easy extended by any other roles. For example role "A" define dict "repos" with repo needed for role "A", and after I imported role "git_role" along with role "A" in playbook. "git_role" will look into extended dict "repos" and going clone all described repos.

Role "B" for example can described another repo in dict "repos" and "git_role" in that case clone that repos needed for role "B" and so on.

Ansible dy default has :

hash_behaviour=replace

And I don't want change it, as developers don't suggest it. reddit conversation

I look into "combine" but it also override dict...

Who can suggest right painless way to extend one particular dict by any sources?

Not even dict must. There can be any data source which "git_role" can consume and clone all specified repos.


Solution

  • (ansible 2.7.9)

    It is possible to combine dictionaries from different roles. Let's have roles role_A, role_B, and role_C with default variables.

    $ cat roles/role_A/defaults/main.yml
    repos_A:
      name_of_repo_A:
        url: "git@myrepo.com"
        path: "/path/on/local_system"
        branch: "branch_to_checkout"
    
    $ cat roles/role_B/defaults/main.yml
    repos_B:
      name_of_repo_B:
        url: "git@myrepo.com"
        path: "/path/on/local_system"
        branch: "branch_to_checkout"
    
    $ cat roles/role_C/defaults/main.yml
    repos_C:
      name_of_repo_C:
        url: "git@myrepo.com"
        path: "/path/on/local_system"
        branch: "branch_to_checkout"
    

    If the names of the dictionaries follow the names of the roles repos_<SECOND-PART-OF-ROLE-NAME> then the play below

    - hosts: localhost
      roles:
        - role_A
        - role_B
        - role_C
      tasks:
        - set_fact:
            repos: "{{ repos|default({})|
                       combine(lookup('vars',
                                      'repos_' ~ item.split('_').1,
                                       default={}))
                       }}"
          loop: "{{ role_names }}"
        - debug:
            var: repos
    

    gives the combined directory

    "repos": {
        "name_of_repo_A": {
            "branch": "branch_to_checkout", 
            "path": "/path/on/local_system", 
            "url": "git@myrepo.com"
        }, 
        "name_of_repo_B": {
            "branch": "branch_to_checkout", 
            "path": "/path/on/local_system", 
            "url": "git@myrepo.com"
        }, 
        "name_of_repo_C": {
            "branch": "branch_to_checkout", 
            "path": "/path/on/local_system", 
            "url": "git@myrepo.com"
        }
    }
    

    Control repos inside roles

    It is possible to control the repos inside the roles. Let's put the tasks below into the roles and introduce the variable repos_source

    $ cat roles/role_A/tasks/main.yml
    - set_fact:
        repos: {}
    - set_fact:
        repos: "{{ repos|
                   combine(lookup('vars',
                                  'repos_' ~ item.split('_').1,
                                   default={}))
                    }}"
      loop: "{{ repos_source }}"
    - debug:
        var: repos
    

    The tasks below

    - import_role:
        name: role_A
      vars:
        repos_source:
          - role_A
          - role_C
    
    - import_role:
        name: role_B
      vars:
        repos_source:
          - role_A
          - role_B
    
    - import_role:
        name: role_C
      vars:
        repos_source:
          - role_B
          - role_C
    

    give

    TASK [role_A : debug] **********************************************************************************
    ok: [localhost] => {
        "repos": {
            "name_of_repo_A": {
                "branch": "branch_to_checkout", 
                "path": "/path/on/local_system", 
                "url": "git@myrepo.com"
            }
            "name_of_repo_C": {
                "branch": "branch_to_checkout", 
                "path": "/path/on/local_system", 
                "url": "git@myrepo.com"
            }
        }
    }
    
    TASK [role_B : debug] **********************************************************************************
    ok: [localhost] => {
        "repos": {
            "name_of_repo_A": {
                "branch": "branch_to_checkout", 
                "path": "/path/on/local_system", 
                "url": "git@myrepo.com"
            }, 
            "name_of_repo_B": {
                "branch": "branch_to_checkout", 
                "path": "/path/on/local_system", 
                "url": "git@myrepo.com"
            }
        }
    }
    
    TASK [role_C : debug] **********************************************************************************
    ok: [localhost] => {
        "repos": {
            "name_of_repo_B": {
                "branch": "branch_to_checkout", 
                "path": "/path/on/local_system", 
                "url": "git@myrepo.com"
            }, 
            "name_of_repo_C": {
                "branch": "branch_to_checkout", 
                "path": "/path/on/local_system", 
                "url": "git@myrepo.com"
            }
        }
    }
    


    NOTE In ansible 2.8 use ansible_play_role_names or ansible_role_names. See Special Variables.