--- - name: Prepare YAML exporter updates ansible.builtin.set_fact: _prometheus_target_yaml_updates: >- {%- set ns = namespace(updates=[]) -%} {%- set exporters = prometheus_target_exporter + ([] if prometheus_target_skip_default_exporters else prometheus_target_default_exporters) -%} {%- for exporter in exporters -%} {%- set defaults = prometheus_target_exporter_defaults[exporter.id] | default({}) if exporter.id is defined else {} -%} {%- set path = (exporter.path_prefix if exporter.path_prefix is defined else (defaults.path_prefix | default(prometheus_target_exporter_target_prefix))) ~ ((exporter.path if exporter.path is defined else defaults.path) | mandatory) -%} {%- set host = (exporter.host if exporter.host is defined else defaults.host) | mandatory -%} {%- set labels = (defaults.labels | default({})) | combine(exporter.labels | default({})) -%} {%- set target_state = exporter.state if exporter.state is defined else (defaults.state | default('present')) -%} {%- set _ = ns.updates.append( {'path': path, 'host': host, 'labels': labels, 'state': target_state}) -%} {%- endfor -%} {{ ns.updates }} - name: Validate YAML exporter states ansible.builtin.assert: that: _yaml_update.state in ["present", "absent"] fail_msg: "Exporter state must be 'present' or 'absent', got '{{ _yaml_update.state }}'" loop: "{{ _prometheus_target_yaml_updates }}" loop_control: loop_var: _yaml_update label: "{{ _yaml_update.path }}" # Every play host prepares its values above. Applying all updates from one host # prevents parallel Ansible forks from overwriting each other's YAML changes. - name: Aggregate YAML exporter updates ansible.builtin.set_fact: _prometheus_target_yaml_updates_all: >- {%- set ns = namespace(updates=[]) -%} {%- for target_host in ansible_play_batch -%} {%- for update in hostvars[target_host]._prometheus_target_yaml_updates -%} {%- set _ = ns.updates.append(update) -%} {%- endfor -%} {%- endfor -%} {{ ns.updates }} run_once: true - name: Collect YAML target paths ansible.builtin.set_fact: _prometheus_target_yaml_paths: >- {{ _prometheus_target_yaml_updates_all | map(attribute='path') | unique | list }} run_once: true - name: Read YAML target files ansible.builtin.slurp: src: "{{ _yaml_path }}" delegate_to: "{{ prometheus_target_host }}" loop: "{{ _prometheus_target_yaml_paths }}" loop_control: loop_var: _yaml_path register: _yaml_target_files failed_when: false run_once: true # Build each destination completely in memory and write it once. Besides # avoiding lost updates, this keeps updates to different files independent. - name: Build updated YAML target files ansible.builtin.set_fact: _prometheus_target_yaml_files: >- {%- set output = namespace(files=[]) -%} {%- for file in _yaml_target_files.results -%} {%- set file_updates = _prometheus_target_yaml_updates_all | selectattr('path', 'equalto', file._yaml_path) | list -%} {%- set present_updates = file_updates | selectattr('state', 'equalto', 'present') | list -%} {%- set state = namespace(groups=(file.content | b64decode | from_yaml) if file.content is defined else []) -%} {%- set state.groups = state.groups if state.groups is not none else [] -%} {%- for update in file_updates -%} {%- set next = namespace(found=false, groups=[]) -%} {%- for group in state.groups -%} {%- set group_labels = group.labels | default({}) -%} {%- if update.state == 'absent' -%} {%- set filtered = group.targets | reject('equalto', update.host) | list -%} {%- if filtered | length > 0 -%} {%- set replacement = {'targets': filtered} -%} {%- if group_labels | length > 0 -%} {%- set _ = replacement.update({'labels': group_labels}) -%} {%- endif -%} {%- set _ = next.groups.append(replacement) -%} {%- endif -%} {%- elif group_labels == update.labels -%} {%- if update.host not in group.targets -%} {%- set _ = group.targets.append(update.host) -%} {%- endif -%} {%- set next.found = true -%} {%- set _ = next.groups.append(group) -%} {%- else -%} {%- set filtered = group.targets | reject('equalto', update.host) | list -%} {%- if filtered | length > 0 -%} {%- set replacement = {'targets': filtered} -%} {%- if group_labels | length > 0 -%} {%- set _ = replacement.update({'labels': group_labels}) -%} {%- endif -%} {%- set _ = next.groups.append(replacement) -%} {%- endif -%} {%- endif -%} {%- endfor -%} {%- if update.state == 'present' and not next.found -%} {%- set new_group = {'targets': [update.host]} -%} {%- if update.labels | length > 0 -%} {%- set _ = new_group.update({'labels': update.labels}) -%} {%- endif -%} {%- set _ = next.groups.append(new_group) -%} {%- endif -%} {%- set state.groups = next.groups -%} {%- endfor -%} {%- if file.content is defined or present_updates | length > 0 -%} {%- set _ = output.files.append({'path': file._yaml_path, 'groups': state.groups}) -%} {%- endif -%} {%- endfor -%} {{ output.files }} run_once: true - name: Write updated YAML target files ansible.builtin.copy: content: "{{ _yaml_file.groups | to_nice_yaml(indent=2, width=1337) }}" dest: "{{ _yaml_file.path }}" mode: "0644" delegate_to: "{{ prometheus_target_host }}" become: true loop: "{{ _prometheus_target_yaml_files }}" loop_control: loop_var: _yaml_file label: "{{ _yaml_file.path }}" register: _yaml_results run_once: true - name: Track aggregated YAML changes on each host ansible.builtin.set_fact: changed: >- {{ hostvars[ansible_play_batch | first]._yaml_results.results | selectattr('changed') | list | length > 0 }}