Search code examples
ansiblelist-comparison

Ansible: compare two arrays by specific field


I have two arrays with applications descriptions:

source_array:
  - status: Active
    AppName": "Application 1"
    version: "0.1.1"
    metadata: ""
  - status": "Active"
    AppName: "Application 2"
    version: "0.2.2"
    metadata: "ID123"
  - status: "Active"
    AppName: "Application 3"
    version: "0.3.3"
    metadata: ""

And:

target_array:
  - status: "Active"
    AppName: "Application 1"
    version: "0.1.1"
    metadata: ""
  - status: "Active"
    AppName: "Application 2"
    version: "0.2.2"
    metadata: "ID321"
  - status: "Active",
    AppName: "Application 3"
    version: "0.3.0"
    metadata: ""

I need to compare these two arrays based on the version field. So, for example, the desired result should be:

[{
    "status": "Active",
    "AppName": "Application 3",
    "version": "0.3.0",
    "metadata": ""
}]

I've tried to use difference filter but it returns also the secondf element - as it has different metadata

- name: Comparing arrays
  set_fact:
    delta: "{{ source_array | difference(target_array) }}"

And i've got incorrect result:

[{
    "status": "Active",
    "AppName": "Application 2",
    "version": "0.2.2",
    "metadata": "ID123"
},
{
    "status": "Active",
    "AppName": "Application 3",
    "version": "0.3.3",
    "metadata": ""
},
{
    "status": "Active",
    "AppName": "Application 2",
    "version": "0.2.2",
    "metadata": "ID321"
},
{
    "status": "Active",
    "AppName": "Application 3",
    "version": "0.3.0",
    "metadata": ""
}]

Any help will be highly appreciated!


Solution

  • This not trivial indeed. You don't provide much context but I suspect what you want to do is something like check if an application has been or should be updated. Right ?

    Here is one way:

    - hosts: localhost
      vars:
        array1:
          - status: "Active"
            AppName: "Application 1"
            version: "0.1.1"
            metadata: ""
          - status: "Active"
            AppName: "Application 2"
            version: "0.2.2"
            metadata: "ID321"
          - status: "Active"
            AppName: "Application 3"
            version: "0.3.3"
            metadata: ""
        array2:
          - status: "Active"
            AppName: "Application 1"
            version: "0.1.1"
            metadata: ""
          - status: "Active"
            AppName: "Application 2"
            version: "0.2.2"
            metadata: "ID321"
          - status: "Active"
            AppName: "Application 3"
            version: "0.3.0"
            metadata: ""
    
      tasks:
        - name: "Show matching pattern"
          debug:
            msg: "{{'^' + (array1|map(attribute='version'))|difference(array2|map(attribute='version'))|join('|') + '$'}}"
    
        - name: "Compare arrays"
          debug:
            msg: "{{ array1 | selectattr('version', 'match', '^' + (array1|map(attribute='version'))|difference(array2|map(attribute='version'))|join('|') + '$') | list }}"
    

    It works by first finding the "newer versions", then screening the original list based on those. But it is a bit brittle cause:

    • it assumes you now, a priori which array contains the "newer" data (here all newer version are in array1).
    • if you have two or more elements in your array with identical versions it would retain both, not knowing which to chose.

    Maybe you should consider a different data structure, like a mapping (dict). See the current_state variable below:

    - hosts: localhost
      vars:
        current_state:
          "Application 1":
            status: "Active"
            version: "0.1.1"
            metadata: ""
          "Application 2":
            status: "Active"
            version: "0.2.2"
            metadata: "ID321"
          "Application 3":
            status: "Active"
            version: "0.3.0"
            metadata: ""
        new_applications:
          - status: "Active"
            AppName: "Application 1"
            version: "0.1.1"
            metadata: ""
          - status: "Active"
            AppName: "Application 2"
            version: "0.2.2"
            metadata: "ID321"
          - status: "Active"
            AppName: "Application 3"
            version: "0.3.3"
            metadata: ""
          - status: "Active"
            AppName: "Application 4"
            version: "0.1.0"
            metadata: ""
      tasks:
        - name: "Different appraoch"
          debug:
             msg: "{{ item.0 }} -- {{ item.1 }} -- Should update: {{ item.1.version is version((current_state[item.0]|default({'version': '0.0.0'}))['version'], '>') }}"
          loop: "{{ new_applications|map(attribute='AppName')|zip(new_applications)|list }}"
    
        - name: "Build 'current_state' from a list (if not available as is)"
          # There might be a smarter way using items2dict...
          set_fact:
            dict_from_list: "{{ dict_from_list|default({})|combine({item[0]: item[1]})}}"
          loop: "{{ new_applications|map(attribute='AppName')|zip(new_applications)|list }}"
    
        - debug:
            var: dict_from_list
    

    This version fixes the last of the two issues mentioned above. It is also more robust in case the order of the two arrays are not the same, or the array do not have the same length.

    The first issue I chose to ignore because, though your question lead to believe array1 and array2 to be interchangeable, I am assuming they are in fact not, within a given context.