---

# Refer <https://docs.ansible.com/ansible/latest/playbook_guide/playbooks_filters.html>.

- name: Convert between types
  tags:
    - convert_types
    - type_conversion
  ansible.builtin.set_fact:
    string_to_int_is_integer: "{{ 'string' | int is integer }}"
    string_to_float_is_float: "{{ 'string' | float is float }}"
    integer_to_float_is_float: "{{ 12 | float is float }}"
    float_to_int_is_integer: "{{ 21.02 | int is integer }}"
    integer_to_string_is_string: "{{ 43 | string is string }}"
    float_to_string_is_string: "{{ 74.93 | string is string }}"
    integer_to_bool_is_boolean: "{{ 4 | bool is boolean }}"

- name: Manipulate dictionaries
  tags:
    - dictionary_manipulation
    - manipulate_dictionaries
  vars:
    organization:
      address: 123 common lane
      id: 123abc
  block:
    - name: Add keys to dictionaries
      ansible.builtin.set_fact:
        organization: "{{ organization | combine({ 'name': 'ExampleOrg' }) }}"
    - name: Sort keys in dictionaries
      ansible.builtin.set_fact:
        organization: "{{ organization | dictsort }}"
    - name: Pretty print dictionaries
      ansible.builtin.set_fact:
        organization: "{{ organization | to_nice_json }}"
    - name: Merge dictionaries
      vars:
        dict_1:
          a: 43
          b: some string
        dict_2:
          y: true
          z:
            - 4
            - test
      ansible.builtin.set_fact:
        merged_dict: "{{ dict_1 | ansible.builtin.combine(dict_2, {'z':'new_value','w':[44]}) }}"
        recursively_merged_dict: >-
          {{ {'rest':'test'} | ansible.builtin.combine({'z':'newValue','w':[44]}, dict_1, dict_2, recursive=true) }}
    - name: Register the list of extensions per DB
      tags: never
      ansible.builtin.set_fact:
        db_extensions: >-
          {{
            db_extensions
            | combine({
                item.item: item.query_result | map(attribute='extname')
            })
          }}
      with_items: "{{ db_extensions_query.results }}"
    - name: Register the list of extensions per DB as 'db:extensions[]' pairs
      vars:
        db_extensions:
          sales:
            - pgaudit
            - plpgsql
          countries:
            - pgcrypto
            - postgis
            - pg_stat_statements
      ansible.builtin.set_fact:
        db_extension_pairs:
          # Refer https://jinja.palletsprojects.com/en/3.0.x/templates/#assignments for the namespace object's
          # reason
          >-
            {%- set ns = namespace(output = []) -%}
            {%- for db in db_extensions.keys() -%}
              {%- for extension in db_extensions[db] -%}
                {{- ns.output.append({'db':db, 'extension': extension}) -}}
              {%- endfor -%}
            {%- endfor -%}
            {{- ns.output -}}
    - name: Create a list of hosts patterns from an EC2 instance's tags
      vars:
        ec2_instance:
          instance_id: i-01234567890abcdef
          tags:
            Application: PostgreSQL
            Component: DB restorer
            RestoreSource: s3://some-bucket/db/backups/test_2024-12-31.sqlc
            RestoreTarget: restored-test
      ansible.builtin.set_fact:
        hosts_pattern:
          # if 'ec2_instance' exists
          #   return 'instance_id' if 'ec2_instance' has it
          # else if 'ec2_instance' has tags
          #   return a ',&'-separated list of 'tag_tagKey_tagValue' strings
          # else
          #   return 'all'
          >-
            {% set ns = namespace(tag_based_patterns = []) %}
            {% for k, v in (ec2_instance.tags | default({}) | items) %}
            {{
                ns.tag_based_patterns.append(
                  ['tag', k, v] | join('_') | regex_replace('[ :/\-\.]', '_')
                )
            }}
            {% endfor %}
            {{
              [
                ec2_instance.instance_id | default(None),
                ns.tag_based_patterns | join(',&'),
              ] | select | first | default('all')
            }}
    - name: Get the device name and last snapshot id for all block devices in an EC2 instance
      # Useful to create AMIs from instance snapshots
      tags: never
      ansible.builtin.set_fact:
        last_snap_for_device:
          # Refer https://jinja.palletsprojects.com/en/3.0.x/templates/#assignments for the namespace object's
          # reason
          >-
            {%- set ns = namespace(devices_list = []) -%}
            {%- for result in current_instance_snapshots.results -%}
            {%-   for device in current_instance_information.instances[0].block_device_mappings
                  | selectattr('ebs.volume_id', 'equalto', result.volume_id) -%}
            {{-
                    ns.devices_list.append({
                      'device_name': device.device_name,
                      'snapshot_id': result.snapshots
                      | sort(attribute='start_time') | last
                      | json_query('snapshot_id'),
                    })
            -}}
            {%-   endfor -%}
            {%- endfor -%}
            {{  ns.devices_list }}

- name: Manipulate lists
  tags:
    - list_manipulation
    - manipulate_lists
  vars:
    mounts:
      - block_available: 237681860
        block_size: 4096
        block_total: 239859712
        block_used: 2177852
        device: /dev/mapper/vg0-root
        fstype: btrfs
        inode_available: 0
        inode_total: 0
        inode_used: 0
        mount: /
        options: rw,relatime,compress-force=zstd:15,ssd,space_cache,autodefrag,subvolid=256,subvol=/os,bind
        size_available: 973544898560
        size_total: 982465380352
        uuid: 6fc42685-8f3f-43a3-b698-a135b2b59ac5
      - block_available: 110948
        block_size: 4096
        block_total: 130812
        block_used: 19864
        device: /dev/sda1
        fstype: vfat
        inode_available: 0
        inode_total: 0
        inode_used: 0
        mount: /boot
        options: rw,relatime,fmask=0022,dmask=0022,codepage=437,iocharset=iso8859-1,shortname=mixed,utf8,errors=remount-ro
        size_available: 454443008
        size_total: 535805952
        uuid: 8ACA-ADB8
    snapshots_list:
      - name: some_available_snapshot
        status: available
      - name: some_unavailable_snapshot
        status: creating
  block:
    - name: Add elements to lists
      vars:
        programming_languages:
          - C
          - Python
      ansible.builtin.set_fact:
        programming_languages: "{{ programming_languages + ['Ruby'] }}"
    - name: Remove elements from lists
      vars:
        dbs_list: ['primary', 'sales']
      ansible.builtin.set_fact:
        list_without_items: "{{ dbs_list | difference(['template0','template1','postgres','rdsadmin']) }}"
    - name: Get elements
      ansible.builtin.set_fact:
        random_item: "{{ ['a','b','c'] | random }}"
        last_item_array_mode: "{{ ['a','b','c'][-1] }}"
        last_item_filter: "{{ ['a','b','c'] | last }}"
        first_item_filter: "{{ ['a','b','c'] | first }}"
    - name: Sort dict elements in lists by attribute
      tags: order_by
      vars:
        snapshots:
          - name: sales
            create_time: '2024-06-25T00:52:55.127000+00:00'
          - name: test
            create_time: '2024-05-17T01:53:12.103220+00:00'
      ansible.builtin.set_fact:
        snapshot_latest: "{{ snapshots | sort(attribute='create_time') | last }}"
    - name: Give back the first not null value
      tags: coalesce
      vars:
        list_with_null_values:
          - null
          - null
          - something
          - something else
      ansible.builtin.set_fact:
        first_non_null_value: "{{ list_with_null_values | select | first }}"
    - name: Get values for a specific attribute in a list of dictionaries
      vars:
        instances_information:
          instances:
            - block_device_mappings:
                - ebs:
                    volume_id: vol-0123456
              vpc_security_groups:
                - vpc_security_group_id: sg-0123456789abcdef
        instance_information: "{{ instances_information.instances[0] }}"
      ansible.builtin.set_fact:
        vpc_security_group_ids: >-
          {{ instance_information.vpc_security_groups | map(attribute='vpc_security_group_id') }}
        volume_ids: >-
          {{ instances_information.instances[0].block_device_mappings | map(attribute='ebs.volume_id') }}
    - name: Return only elements with specific attributes matching a filter
      ansible.builtin.set_fact:
        available_rds_snapshots: "{{ snapshots_list | selectattr('status', 'equalto', 'available') }}"
        mounts_with_path: "{{ mounts | selectattr('mount', 'in', ['/boot']) }}"
    - name: Return all elements *but* the ones with specific attributes matching a filter
      ansible.builtin.set_fact:
        available_rds_snapshots: "{{ snapshots_list | rejectattr('status', 'equalto', 'creating') }}"
        mounts_without_path: "{{ mounts | rejectattr('mount', 'in', ['/boot']) }}"
    - name: Remove lines about RDS protected users and permissions from a dump file
      tags: never
      # remove empty lines
      # remove comments
      # remove creation of the master user
      # remove anything involving 'rdsadmin'
      # remove changes to protected RDS users
      # remove protected 'superuser' and 'replication' assignments
      vars:
        # **Hack notice**: Ansible has issues with splitting on new lines if this template is quoted differently
        permissions_dump_content_as_lines: "{{ dump_file.content | ansible.builtin.b64decode | split('\n') }}"
        master_username: postgresql
      ansible.builtin.set_fact:
        permissions_commands: >-
          {{
            permissions_dump_content_as_lines
            | reject('match', '^$')
            | reject('match', '^--')
            | reject('match', '^CREATE ROLE ' + master_username)
            | reject('match', '.*rdsadmin.*')
            | reject('match', '^(CREATE|ALTER) ROLE rds_')
            | map('regex_replace', '(\s+(NO)?(SUPERUSER|REPLICATION))?', '')
          }}

- name: Manipulate numbers
  tags:
    - manipulate_numbers
    - number_manipulation
  ansible.builtin.set_fact:
    max_of_n: "{{ [ 3, 4, 2 ] | max }}"
    max_of_values: "{{ [{'val': 1}, {'val': 2}] | max(attribute='val') }}"
    min_of_n: "{{ [ 3, 4, 2 ] | min }}"
    min_of_values: "{{ [{'val': 1}, {'val': 2}] | min(attribute='val') }}"
    round: "{{ 42.21 | round }}"
    round_up: "{{ 42.21 | round(method='ceil') }}"
    round_down_to_2nd_decimal: "{{ (2.5 * (42.2109 / 3) + 1) | round(2, 'floor') }}"
    round_to_int: "{{ 42.21 | round | int }}"

- name: Manipulate strings
  tags:
    - manipulate_strings
    - string_manipulation
  vars:
    module_output: >-
      u001b]0;@smth:/u0007{
      "failed": 0, "started": 1, "finished": 0, "ansible_job_id": "j968817333249.114504",
      "results_file": "/home/ssm-user/.ansible_async/j968817333249.114504", "_ansible_suppress_tmpdir_delete": true
      }\r\r
    pattern: >-
      {{ '"failed": 0, "started": 1, "finished": 0' | regex_escape() }}
  ansible.builtin.set_fact:
    first_letter_to_uppercase: "{{ 'all_lowercase' | capitalize }}"
    something_replaced: "{{ 'dots.to.dashes' | replace('.','-') }}"
    split_string: "{{ 'testMe@example.com' | split('@') | first }}"
    pattern_replaced: >-
      {{ '*.domain.com...' | regex_replace('*' | regex_escape, 'star') | regex_replace('\.+$', '') }}
    pattern_is_anywhere_in_module_output: "{{ module_output is search(pattern) }}"
    pattern_is_at_the_beginning_of_string: "{{ 'sator arepo tenet opera rotas' is match('sator arepo') }}"
    regex_is_anywhere_in_string: "{{ 'sator arepo tenet opera rotas' is regex('\\stenet\\s') }}"
    first_substr_matching_regex: "{{ 'sator arepo tenet opera rotas' | regex_search('\\stenet\\s') }}"
    password_obfuscated: "{{ 'sensitiveString' | regex_replace('^(.{2}).*(.{2})$', '\\1…\\2') }}"
    value_from_json_string_in_module_output: >-
      {{ 'ansible_job_id' | extract(module_output | regex_search('{.*}') | from_json) }}
    base64_encoded_string: "{{ 'some string' | ansible.builtin.b64encode }}"
    base64_decoded_string: "{{ 'c29tZSBzdHJpbmc=' | ansible.builtin.b64decode }}"
    csv_to_command_options: >-
      {{
        [
          pg_shared_preload_libraries|split(',')|reject('equalto','pgaudit')|map('regex_replace','^','--extension='),
          '--no-publications',
          '--no-subscriptions',
          '--exclude-schema=transient_views',
        ] | flatten | unique
      }}
    raw_string: !unsafe "{{ this template is not executed, but given as-is as string }}"
    raw_string_via_jinja_specification: "{% raw -%} 234%234{435lkj{{lkjsdf {%- endraw %}"

- name: Return data types
  tags:
    - return_type
    - type_return
  ansible.builtin.set_fact:
    returns_AnsibleUndefined: "{{ null | type_debug }}"
    returns_int: "{{ 3 | type_debug }}"
    returns_NoneType: "{{ None | type_debug }}"
    returns_str: "{{ 'this' | type_debug }}"

- name: Test data types
  tags:
    - test_types
    - type_test
  ansible.builtin.set_fact:
    # strings are classified as 'string', 'iterable' and 'sequence', but not 'mapping'
    aa_is_string: "{{ 'aa' is string }}"
    aa_is_iterable: "{{ 'aa' is iterable }}"
    aa_is_sequence: "{{ 'aa' is sequence }}"
    # numbers are classified as 'numbers', with 'integer' and 'float' being subclasses
    i42_is_number: "{{ 42 is number }}"
    i5_is_integer: "{{ 5 is integer }}"
    f21_34_is_number: "{{ 21.34 is number }}"
    f12_1_is_float: "{{ 12.1 is float }}"
    # lists are classified as 'iterable' and 'sequence', but not as 'string' nor 'mapping'
    list_is_iterable: "{{ ['list'] is iterable }}"
    list_is_sequence: "{{ ['list'] is sequence }}"
    list_is_string: "{{ ['list'] is string }}"
    list_is_mapping: "{{ ['list'] is mapping }}"
    # dictionaries are classified as 'iterable', 'sequence' and 'mapping', but not as 'string'
    dict_is_iterable: "{{ {'a': 'dict'} is iterable }}"
    dict_is_sequence: "{{ {'a': 'dict'} is sequence }}"
    dict_is_mapping: "{{ {'a': 'dict'} is mapping }}"
    dict_is_string: "{{ {'a': 'dict'} is string }}"
    # native booleans
    true_is_boolean: "{{ true is boolean }}"
    upper_true_is_boolean: "{{ True is boolean }}"
    false_is_boolean: "{{ false is boolean }}"
    upper_false_is_boolean: "{{ False is boolean }}"
    # null ~= None in ansible (python gotcha)
    aa_is_not_NoneType: "{{ 'aa' | type_debug != 'NoneType' }}"
    aa_is_not_null: "{{ 'aa' != None }}"  # same as 'aa_is_not_NoneType' but easier to read
    aa_is_not_null_nor_empty: "{{ 'aa' not in [ None, '' ] }}"

- name: Test truthfulness
  tags:
    - test_truthfulness
    - truthfulness
  ansible.builtin.set_fact:
    this_is_true: true
    this_is_false: false
    this_is_true_again: "{{ not false }}"
    true_is_truthy: "{{ true is truthy }}"
    false_is_falsy: "{{ false is falsy }}"
    any_element_in_list_is_truthy_results_false: "{{ [false, '', None, 0] is any }}"
    all_elements_in_list_are_truthy_results_true: "{{ [true, 'some string', 1] is all }}"

- name: Undefined variables
  tags: undefined_variable
  # refer <https://docs.ansible.com/ansible/latest/playbook_guide/playbooks_templating_undef.html>
  # only works for variables of lesser priority
  # e.g., works in tasks' `vars:`, but not in the `set_facts` module to (re)define a variable
  vars:
    test_var: "{{ undef() }}"
  ansible.builtin.debug:
    var: test_var
