Ansible: compare two arrays by specific field

user41995 picture user41995 · May 31, 2019 · Viewed 7.2k times · Source

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!

Answer

Cans picture Cans · May 31, 2019

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.