aboutsummaryrefslogtreecommitdiffstatshomepage
path: root/molecule
diff options
context:
space:
mode:
Diffstat (limited to 'molecule')
-rw-r--r--molecule/default/converge.yml506
-rw-r--r--molecule/default/molecule.yml16
-rw-r--r--molecule/default/tests/test_check_prometheus_targets.py90
-rw-r--r--molecule/default/tests/test_lineinfile_strategy.py94
-rw-r--r--molecule/default/tests/test_yaml_strategy.py139
5 files changed, 667 insertions, 178 deletions
diff --git a/molecule/default/converge.yml b/molecule/default/converge.yml
index 98d7e8a..4e379da 100644
--- a/molecule/default/converge.yml
+++ b/molecule/default/converge.yml
@@ -1,156 +1,486 @@
---
################################################################################
################################################################################
-- name: Simple deploy
+- name: Lineinfile bootstrap and subsequent hosts
hosts: application
pre_tasks:
- - name: Create targets
- ansible.builtin.file:
- path: "{{ item }}"
- state: touch
- modification_time: preserve
- access_time: preserve
- mode: "0644"
+ - name: Create test user
+ ansible.builtin.user:
+ name: prometheus
+ become: true
+ delegate_to: "{{ prometheus_target_host }}"
+
+ - name: Create lineinfile bootstrap target
+ ansible.builtin.copy:
+ dest: /opt/lineinfile_bootstrap.yml
+ owner: prometheus
+ group: prometheus
+ mode: "0600"
+ content: |
+ - labels:
+ my: label
+ targets:
+ - existing:9100
+ force: false
become: true
delegate_to: "{{ prometheus_target_host }}"
- loop:
- - /opt/simple_target1.yml
- - /opt/simple_target2.yml
- - /opt/simple_target3.yml
- - /opt/simple_target4.yml
vars:
prometheus_target_host: prometheus
- prometheus_target_exporter_defaults:
- node_exporter:
- path: /opt/simple_target1.yml
- host: "{{ inventory_hostname }}"
- blackbox_exporter:
- path: /opt/simple_target2.yml
- host: "{{ inventory_hostname }}_AA"
+ prometheus_target_strategy_lineinfile_prefix: " - "
+ prometheus_target_strategy_lineinfile_suffix: :9100
roles:
- role: kliwniloc.prometheus_target
prometheus_target_exporter:
- - id: node_exporter
- - { id: blackbox_exporter, host: test1 }
- - { id: blackbox_exporter, host: test2 }
- - { host: exporter_without_id, path: /opt/simple_target4.yml }
- - role: kliwniloc.prometheus_target
- prometheus_target_exporter:
- - id: blackbox_exporter
- path: /opt/simple_target3.yml
+ - path: /opt/lineinfile_bootstrap.yml
+ host: "{{ inventory_hostname }}"
################################################################################
################################################################################
-- name: Prefix deploy
+- name: Lineinfile first host on empty file
hosts: application
pre_tasks:
- - name: Create targets
- ansible.builtin.file:
- path: "{{ item.path }}"
- state: "{{ item.state }}"
- modification_time: preserve
- access_time: preserve
- mode: "0644"
+ - name: Create empty lineinfile target
+ ansible.builtin.copy:
+ dest: /opt/lineinfile_first_host.yml
+ owner: prometheus
+ group: prometheus
+ mode: "0600"
+ content: ""
+ force: false
become: true
delegate_to: "{{ prometheus_target_host }}"
- loop:
- - { state: touch, path: /opt/prefix_target1.yml }
- - { state: touch, path: /opt/prefix_target2.yml }
- - { state: directory, path: /opt/prefix}
- - { state: touch, path: /opt/prefix/prefix_target3.yml }
vars:
prometheus_target_host: prometheus
- prometheus_target_exporter_target_prefix: /opt/
- prometheus_target_exporter_defaults:
- node_exporter:
- path: prefix_target1.yml
- host: "{{ inventory_hostname }}"
- blackbox_exporter:
- path: prefix_target_not_exist.yml
- host: "{{ inventory_hostname }}"
- path_prefix: /opt/prefix/
+ prometheus_target_strategy_lineinfile_prefix: "- "
+ prometheus_target_strategy_lineinfile_suffix: :9100
roles:
- role: kliwniloc.prometheus_target
prometheus_target_exporter:
- - id: node_exporter
- - { id: node_exporter, path: /opt/prefix_target2.yml, path_prefix: "" }
- - { id: blackbox_exporter, path: prefix_target3.yml }
+ - path: /opt/lineinfile_first_host.yml
+ host: "{{ inventory_hostname }}"
################################################################################
################################################################################
-- name: Deploy with hooks
+- name: Lineinfile subsequent hosts and parallel writes
hosts: application_group
pre_tasks:
- - name: Create targets
- ansible.builtin.file:
- path: "{{ item }}"
- state: touch
- modification_time: preserve
- access_time: preserve
- mode: "0644"
+ - name: Create lineinfile parallel target
+ ansible.builtin.copy:
+ dest: /opt/lineinfile_parallel.yml
+ owner: prometheus
+ group: prometheus
+ mode: "0600"
+ content: ""
+ force: false
become: true
delegate_to: "{{ prometheus_target_host }}"
- loop:
- - /opt/hook_target.yml
- - /opt/hook2
+ run_once: true
vars:
prometheus_target_host: prometheus
- prometheus_target_handler_command_enabled: true
- prometheus_target_handler_command:
- cmd: mkdir /opt/hook1 # Fails if run twice
- prometheus_target_handler_command_run_once: true
- prometheus_target_handler_shell_enabled: true
- prometheus_target_handler_shell:
- cmd: echo hello >> /opt/hook2
+ prometheus_target_strategy_lineinfile_prefix: "- "
+ prometheus_target_strategy_lineinfile_suffix: :9100
roles:
- role: kliwniloc.prometheus_target
prometheus_target_exporter:
- - id: node_exporter
+ - path: /opt/lineinfile_parallel.yml
host: "{{ inventory_hostname }}"
- path: /opt/hook_target.yml
################################################################################
################################################################################
-- name: Deploy with lineinfile
- hosts: application
+- name: Lineinfile user edited file
+ hosts: application2
pre_tasks:
- - name: Create test user
- ansible.builtin.user:
- name: prometheus
- become: true
+ - name: Create user edited lineinfile target
+ ansible.builtin.copy:
+ dest: /opt/lineinfile_user_edited.yml
+ owner: prometheus
+ group: prometheus
+ mode: "0600"
+ content: |
+ # user comment
+ - labels:
+ env: prod
+ targets:
+ - existing:9100
+ force: false
delegate_to: "{{ prometheus_target_host }}"
- - name: Create target
+ vars:
+ prometheus_target_host: prometheus
+ prometheus_target_strategy_lineinfile_prefix: " - "
+ prometheus_target_strategy_lineinfile_suffix: :9100
+
+ roles:
+ - role: kliwniloc.prometheus_target
+ prometheus_target_exporter:
+ - path: /opt/lineinfile_user_edited.yml
+ host: "{{ inventory_hostname }}"
+
+################################################################################
+################################################################################
+- name: Lineinfile remove last host scenario
+ hosts: localhost
+ connection: local
+ gather_facts: false
+ tasks:
+ - name: Create single host lineinfile target
ansible.builtin.copy:
- dest: /opt/lineinfile.yml
+ dest: /opt/lineinfile_remove_last.yml
owner: prometheus
group: prometheus
mode: "0600"
+ content: |
+ - labels:
+ env: stage
+ targets:
+ - application:9100
+ force: false
+ become: true
+ delegate_to: prometheus
+
+ - name: Remove last host line externally
+ ansible.builtin.lineinfile:
+ path: /opt/lineinfile_remove_last.yml
+ line: " - application:9100"
+ state: absent
+ become: true
+ delegate_to: prometheus
+
+################################################################################
+################################################################################
+- name: YAML bootstrap and first host
+ hosts: application
+ pre_tasks:
+ - name: Create empty yaml bootstrap target
+ ansible.builtin.copy:
+ dest: /opt/yaml_bootstrap.yml
+ mode: "0644"
+ content: ""
force: false
- # yamllint disable rule:indentation
+ become: true
+ delegate_to: "{{ prometheus_target_host }}"
+
+ vars:
+ prometheus_target_host: prometheus
+ prometheus_target_strategy: yaml
+
+ roles:
+ - role: kliwniloc.prometheus_target
+ prometheus_target_exporter:
+ - path: /opt/yaml_bootstrap.yml
+ host: "{{ inventory_hostname }}:9100"
+ labels:
+ job: node
+
+################################################################################
+################################################################################
+- name: YAML subsequent hosts and parallel writes
+ hosts: application_group
+ pre_tasks:
+ - name: Create yaml parallel target
+ ansible.builtin.copy:
+ dest: /opt/yaml_parallel.yml
+ mode: "0644"
content: |
- labels:
- my: label
+ job: node
targets:
- - existing:9100
- # yamllint enable rule:indentation
+ - existing:9100
+ force: false
+ delegate_to: "{{ prometheus_target_host }}"
+ run_once: true
+
+ vars:
+ prometheus_target_host: prometheus
+ prometheus_target_strategy: yaml
+
+ roles:
+ - role: kliwniloc.prometheus_target
+ prometheus_target_exporter:
+ - path: /opt/yaml_parallel.yml
+ host: "{{ inventory_hostname }}:9100"
+ labels:
+ job: node
+
+################################################################################
+################################################################################
+- name: YAML user edited file exact label matching
+ hosts: application2
+ pre_tasks:
+ - name: Create user edited yaml target
+ ansible.builtin.copy:
+ dest: /opt/yaml_user_edited.yml
+ mode: "0644"
+ content: |
+ # user comment that will be lost after parse/write
+ - labels:
+ severity: warning
+ job: external
+ targets:
+ - existing_host1:9100
+ - labels:
+ severity: critical
+ targets:
+ - application2:9200
+ - targets:
+ - unlabeled:9100
+ force: false
+ delegate_to: "{{ prometheus_target_host }}"
+
+ vars:
+ prometheus_target_host: prometheus
+ prometheus_target_strategy: yaml
+ prometheus_target_exporter_defaults:
+ exact_match:
+ path: /opt/yaml_user_edited.yml
+ host: "{{ inventory_hostname }}:9100"
+ labels:
+ severity: warning
+ job: external
+ move_target:
+ path: /opt/yaml_user_edited.yml
+ host: "{{ inventory_hostname }}:9200"
+ labels:
+ severity: critical
+ team: blue
+ no_labels:
+ path: /opt/yaml_user_edited.yml
+ host: standalone:9300
+
+ roles:
+ - role: kliwniloc.prometheus_target
+ prometheus_target_exporter:
+ - id: exact_match
+ - id: move_target
+ - id: no_labels
+
+################################################################################
+################################################################################
+- name: YAML remove last host by moving target
+ hosts: application
+ pre_tasks:
+ - name: Create yaml remove last host target
+ ansible.builtin.copy:
+ dest: /opt/yaml_move_remove_last.yml
+ mode: "0644"
+ content: |
+ - labels:
+ job: old
+ targets:
+ - application:9400
+ force: false
become: true
delegate_to: "{{ prometheus_target_host }}"
vars:
prometheus_target_host: prometheus
- prometheus_target_strategy_lineinfile_prefix: " - "
- prometheus_target_strategy_lineinfile_suffix: :9100
+ prometheus_target_strategy: yaml
+
+ roles:
+ - role: kliwniloc.prometheus_target
+ prometheus_target_exporter:
+ - path: /opt/yaml_move_remove_last.yml
+ host: "{{ inventory_hostname }}:9400"
+ labels:
+ job: new
+
+################################################################################
+################################################################################
+- name: YAML branch coverage matrix
+ hosts: application3
+ pre_tasks:
+ - name: Create yaml branch coverage target
+ ansible.builtin.copy:
+ dest: /opt/yaml_branch_matrix.yml
+ mode: "0644"
+ content: |
+ - labels:
+ job: append
+ env: prod
+ targets:
+ - existing_append:9500
+ - labels:
+ job: wrong
+ env: prod
+ targets:
+ - keep_same_length:9500
+ - labels:
+ job: short
+ targets:
+ - keep_length_mismatch:9500
+ - labels:
+ job: present
+ env: prod
+ targets:
+ - application3:9501
+ - labels:
+ job: move_source
+ targets:
+ - application3:9502
+ - targets:
+ - orphan_unlabeled:9503
+ - targets:
+ - shared_unlabeled:9504
+ - labels:
+ job: foreign
+ targets:
+ - foreign:9505
+ force: false
+ delegate_to: "{{ prometheus_target_host }}"
+
+ vars:
+ prometheus_target_host: prometheus
+ prometheus_target_strategy: yaml
+
+ roles:
+ - role: kliwniloc.prometheus_target
+ prometheus_target_exporter:
+ - path: /opt/yaml_branch_matrix.yml
+ host: application3:9506
+ labels:
+ job: append
+ env: prod
+ - path: /opt/yaml_branch_matrix.yml
+ host: application3:9501
+ labels:
+ job: present
+ env: prod
+ - path: /opt/yaml_branch_matrix.yml
+ host: application3:9502
+ labels:
+ job: move_target
+ - path: /opt/yaml_branch_matrix.yml
+ host: orphan_unlabeled:9503
+ labels:
+ job: promoted
+ - path: /opt/yaml_branch_matrix.yml
+ host: standalone_branch:9504
+ - path: /opt/yaml_branch_matrix.yml
+ host: shared_unlabeled:9504
+
+################################################################################
+################################################################################
+- name: YAML missing file creation branches
+ hosts: application4
+ vars:
+ prometheus_target_host: prometheus
+ prometheus_target_strategy: yaml
+
+ roles:
+ - role: kliwniloc.prometheus_target
+ prometheus_target_exporter:
+ - path: /opt/yaml_missing_labeled.yml
+ host: "{{ inventory_hostname }}:9600"
+ labels:
+ team: infra
+ - path: /opt/yaml_missing_unlabeled.yml
+ host: "{{ inventory_hostname }}:9601"
+
+################################################################################
+################################################################################
+- name: Simple compatibility checks
+ hosts: application
+ pre_tasks:
+ - name: Create compatibility target files
+ ansible.builtin.copy:
+ dest: "{{ item }}"
+ content: ""
+ mode: "0644"
+ force: false
+ become: true
+ delegate_to: "{{ prometheus_target_host }}"
+ loop:
+ - /opt/simple_target1.yml
+ - /opt/simple_target2.yml
+ - /opt/simple_target3.yml
+ - /opt/simple_target4.yml
+ - /opt/prefix_target1.yml
+ - /opt/prefix_target2.yml
+
+ - name: Create prefix directory
+ ansible.builtin.file:
+ path: /opt/prefix
+ state: directory
+ mode: "0644"
+ become: true
+ delegate_to: "{{ prometheus_target_host }}"
+
+ - name: Create prefix target
+ ansible.builtin.copy:
+ dest: /opt/prefix/prefix_target3.yml
+ content: ""
+ mode: "0644"
+ force: false
+ become: true
+ delegate_to: "{{ prometheus_target_host }}"
+
+ vars:
+ prometheus_target_host: prometheus
+ prometheus_target_exporter_target_prefix: /opt/
+ prometheus_target_exporter_defaults:
+ node_exporter:
+ path: simple_target1.yml
+ host: "{{ inventory_hostname }}"
+ blackbox_exporter:
+ path: simple_target2.yml
+ host: "{{ inventory_hostname }}_AA"
+ prefixed_exporter:
+ path: prefix_target1.yml
+ host: "{{ inventory_hostname }}"
+ prefixed_other:
+ path: prefix_target_not_exist.yml
+ host: "{{ inventory_hostname }}"
+ path_prefix: /opt/prefix/
roles:
- role: kliwniloc.prometheus_target
prometheus_target_exporter:
- id: node_exporter
- path: /opt/lineinfile.yml
+ - { id: blackbox_exporter, host: test1 }
+ - { id: blackbox_exporter, host: test2 }
+ - { host: exporter_without_id, path: simple_target4.yml }
+ - { id: blackbox_exporter, path: simple_target3.yml }
+ - { id: prefixed_exporter }
+ - {
+ id: prefixed_exporter,
+ path: /opt/prefix_target2.yml,
+ path_prefix: "",
+ }
+ - { id: prefixed_other, path: prefix_target3.yml }
+
+################################################################################
+################################################################################
+- name: Hook compatibility checks
+ hosts: application_group
+ pre_tasks:
+ - name: Create hook target
+ ansible.builtin.copy:
+ dest: /opt/hook_target.yml
+ content: ""
+ mode: "0644"
+ force: false
+ become: true
+ delegate_to: "{{ prometheus_target_host }}"
+
+ vars:
+ prometheus_target_host: prometheus
+ prometheus_target_handler_command_enabled: true
+ prometheus_target_handler_command:
+ cmd: mkdir -p /opt/hook1
+ creates: /opt/hook1
+ prometheus_target_handler_command_run_once: true
+ prometheus_target_handler_shell_enabled: true
+ prometheus_target_handler_shell:
+ cmd: echo hello >> /opt/hook2
+
+ roles:
+ - role: kliwniloc.prometheus_target
+ prometheus_target_exporter:
+ - path: /opt/hook_target.yml
host: "{{ inventory_hostname }}"
diff --git a/molecule/default/molecule.yml b/molecule/default/molecule.yml
index 3ffe0b8..d143303 100644
--- a/molecule/default/molecule.yml
+++ b/molecule/default/molecule.yml
@@ -27,6 +27,22 @@ platforms:
- name: molecule-container-net
groups: [application_group]
+ - name: application3
+ image: ${MOLECULE_DISTRO:-debian:12}
+ dockerfile: Dockerfile.j2
+ pre_build_image: false
+ networks:
+ - name: molecule-container-net
+ groups: [application_group]
+
+ - name: application4
+ image: ${MOLECULE_DISTRO:-debian:12}
+ dockerfile: Dockerfile.j2
+ pre_build_image: false
+ networks:
+ - name: molecule-container-net
+ groups: [application_group]
+
- name: prometheus
image: ${MOLECULE_DISTRO:-debian:12}
dockerfile: Dockerfile.j2
diff --git a/molecule/default/tests/test_check_prometheus_targets.py b/molecule/default/tests/test_check_prometheus_targets.py
deleted file mode 100644
index b9024b4..0000000
--- a/molecule/default/tests/test_check_prometheus_targets.py
+++ /dev/null
@@ -1,90 +0,0 @@
-testinfra_hosts = ["prometheus"]
-
-"""
-Test functionality of defining exporters and default fallbacks
-"""
-
-
-def test_check_hosts_added_simple(host):
- t1 = host.file("/opt/simple_target1.yml")
- t2 = host.file("/opt/simple_target2.yml")
- t3 = host.file("/opt/simple_target3.yml")
- t4 = host.file("/opt/simple_target4.yml")
-
- assert t1.exists
- assert t2.exists
- assert t3.exists
- assert t4.exists
-
- assert t1.content_string == " - application\n"
-
- assert t2.content_string == " - test1\n - test2\n"
-
- assert t3.content_string == " - application_AA\n"
-
- assert t4.content_string == " - exporter_without_id\n"
-
-
-"""
-Test prefix functionality
-"""
-
-
-def test_check_hosts_added_prefix(host):
- t1 = host.file("/opt/prefix_target1.yml")
- t2 = host.file("/opt/prefix_target2.yml")
- t3 = host.file("/opt/prefix/prefix_target3.yml")
-
- assert t1.exists
- assert t2.exists
- assert t3.exists
-
- assert t1.content_string == " - application\n"
-
- assert t2.content_string == " - application\n"
-
- assert t3.content_string == " - application\n"
-
-
-"""
-Test hook functionality
-"""
-
-
-def test_check_hosts_added_hooks(host):
- t1 = host.file("/opt/hook_target.yml")
- t2 = host.file("/opt/hook1")
- t3 = host.file("/opt/hook2")
-
- assert t1.exists
- assert t2.exists
- assert t3.exists
-
- assert (
- t1.content_string == " - application\n - application2\n"
- or t1.content_string == " - application2\n - application\n"
- )
-
- assert t3.content_string == "hello\nhello\n"
-
-
-"""
-Test lineinfile strategy parameters
-"""
-
-
-def test_check_host_added_lineinfile(host):
- t1 = host.file("/opt/lineinfile.yml")
-
- assert t1.user == "prometheus"
- assert t1.group == "prometheus"
- assert t1.mode == 0o600
-
- assert (
- t1.content_string == "- labels:\n"
- " my: label\n"
- " targets:\n"
- " - existing:9100\n"
- " - application:9100\n"
- )
-
diff --git a/molecule/default/tests/test_lineinfile_strategy.py b/molecule/default/tests/test_lineinfile_strategy.py
new file mode 100644
index 0000000..b75a656
--- /dev/null
+++ b/molecule/default/tests/test_lineinfile_strategy.py
@@ -0,0 +1,94 @@
+import yaml
+
+
+testinfra_hosts = ["prometheus"]
+
+
+def read_file(host, path):
+ target = host.file(path)
+ assert target.exists
+ return target
+
+
+def read_yaml_file(host, path):
+ return yaml.safe_load(read_file(host, path).content_string)
+
+
+def test_lineinfile_bootstrap_adds_first_host_without_breaking_yaml(host):
+ target = read_file(host, "/opt/lineinfile_first_host.yml")
+
+ assert target.user == "prometheus"
+ assert target.group == "prometheus"
+ assert target.mode == 0o600
+ assert read_yaml_file(host, "/opt/lineinfile_first_host.yml") == ["application:9100"]
+
+
+def test_lineinfile_adds_subsequent_host_to_existing_yaml(host):
+ target = read_file(host, "/opt/lineinfile_bootstrap.yml")
+
+ assert target.content_string == (
+ "- labels:\n"
+ " my: label\n"
+ " targets:\n"
+ " - existing:9100\n"
+ " - application:9100\n"
+ )
+
+ assert read_yaml_file(host, "/opt/lineinfile_bootstrap.yml") == [
+ {"labels": {"my": "label"}, "targets": ["existing:9100", "application:9100"]}
+ ]
+
+
+def test_lineinfile_parallel_writes_keep_all_hosts_and_parse(host):
+ hosts = sorted(read_yaml_file(host, "/opt/lineinfile_parallel.yml"))
+ assert hosts == [
+ "application2:9100",
+ "application3:9100",
+ "application4:9100",
+ "application:9100",
+ ]
+
+
+def test_lineinfile_user_edited_file_stays_parseable(host):
+ target = read_file(host, "/opt/lineinfile_user_edited.yml")
+
+ assert target.content_string == (
+ "# user comment\n"
+ "- labels:\n"
+ " env: prod\n"
+ " targets:\n"
+ " - existing:9100\n"
+ " - application2:9100\n"
+ )
+
+ assert read_yaml_file(host, "/opt/lineinfile_user_edited.yml") == [
+ {"labels": {"env": "prod"}, "targets": ["existing:9100", "application2:9100"]}
+ ]
+
+
+def test_lineinfile_removing_last_host_still_leaves_parseable_yaml(host):
+ assert read_yaml_file(host, "/opt/lineinfile_remove_last.yml") == [
+ {"labels": {"env": "stage"}, "targets": None}
+ ]
+
+
+def test_lineinfile_compatibility_simple_and_prefix_cases(host):
+ assert read_yaml_file(host, "/opt/simple_target1.yml") == ["application"]
+ assert read_yaml_file(host, "/opt/simple_target2.yml") == ["test1", "test2"]
+ assert read_yaml_file(host, "/opt/simple_target3.yml") == ["application_AA"]
+ assert read_yaml_file(host, "/opt/simple_target4.yml") == ["exporter_without_id"]
+ assert read_yaml_file(host, "/opt/prefix_target1.yml") == ["application"]
+ assert read_yaml_file(host, "/opt/prefix_target2.yml") == ["application"]
+ assert read_yaml_file(host, "/opt/prefix/prefix_target3.yml") == ["application"]
+
+
+def test_lineinfile_hooks_still_run(host):
+ assert sorted(read_yaml_file(host, "/opt/hook_target.yml")) == [
+ "application",
+ "application2",
+ "application3",
+ "application4",
+ ]
+
+ assert host.file("/opt/hook1").exists
+ assert host.file("/opt/hook2").content_string == "hello\nhello\nhello\nhello\n"
diff --git a/molecule/default/tests/test_yaml_strategy.py b/molecule/default/tests/test_yaml_strategy.py
new file mode 100644
index 0000000..89dbdeb
--- /dev/null
+++ b/molecule/default/tests/test_yaml_strategy.py
@@ -0,0 +1,139 @@
+import yaml
+
+
+testinfra_hosts = ["prometheus"]
+
+
+def read_yaml_file(host, path):
+ target = host.file(path)
+ assert target.exists
+ return yaml.safe_load(target.content_string)
+
+
+def get_group_by_labels(groups, labels):
+ for group in groups:
+ if group.get("labels", {}) == labels:
+ return group
+ raise AssertionError(f"Missing target group with labels {labels!r}")
+
+
+def get_unlabeled_group(groups):
+ for group in groups:
+ if group.get("labels", {}) == {}:
+ return group
+ raise AssertionError("Missing unlabeled target group")
+
+
+def has_group_with_labels(groups, labels):
+ return any(group.get("labels", {}) == labels for group in groups)
+
+
+def test_yaml_bootstrap_adds_first_host_to_empty_file(host):
+ assert read_yaml_file(host, "/opt/yaml_bootstrap.yml") == [
+ {"labels": {"job": "node"}, "targets": ["application:9100"]}
+ ]
+
+
+def test_yaml_parallel_writes_keep_all_hosts_in_same_group(host):
+ groups = read_yaml_file(host, "/opt/yaml_parallel.yml")
+ assert get_group_by_labels(groups, {"job": "node"})["targets"] == [
+ "existing:9100",
+ "application:9100",
+ "application2:9100",
+ "application3:9100",
+ "application4:9100",
+ ]
+
+
+def test_yaml_user_edited_file_is_reparsed_and_keeps_expected_semantics(host):
+ groups = read_yaml_file(host, "/opt/yaml_user_edited.yml")
+
+ assert get_group_by_labels(groups, {"severity": "warning", "job": "external"})[
+ "targets"
+ ] == ["existing_host1:9100", "application2:9100"]
+
+ assert get_group_by_labels(groups, {"severity": "critical", "team": "blue"})[
+ "targets"
+ ] == ["application2:9200"]
+
+ assert get_unlabeled_group(groups)["targets"] == ["unlabeled:9100", "standalone:9300"]
+
+
+def test_yaml_moving_host_removes_last_host_from_old_group(host):
+ groups = read_yaml_file(host, "/opt/yaml_move_remove_last.yml")
+
+ assert not has_group_with_labels(groups, {"job": "old"})
+ assert get_group_by_labels(groups, {"job": "new"})["targets"] == ["application:9400"]
+
+
+def test_yaml_output_remains_parseable_after_all_operations(host):
+ for path in [
+ "/opt/yaml_bootstrap.yml",
+ "/opt/yaml_parallel.yml",
+ "/opt/yaml_user_edited.yml",
+ "/opt/yaml_move_remove_last.yml",
+ "/opt/yaml_branch_matrix.yml",
+ "/opt/yaml_missing_labeled.yml",
+ "/opt/yaml_missing_unlabeled.yml",
+ ]:
+ assert read_yaml_file(host, path) is not None
+
+
+def test_yaml_branch_matrix_covers_exact_match_and_append(host):
+ groups = read_yaml_file(host, "/opt/yaml_branch_matrix.yml")
+
+ assert get_group_by_labels(groups, {"job": "append", "env": "prod"})["targets"] == [
+ "existing_append:9500",
+ "application3:9506",
+ ]
+
+ assert get_group_by_labels(groups, {"job": "present", "env": "prod"})["targets"] == [
+ "application3:9501"
+ ]
+
+
+def test_yaml_branch_matrix_keeps_nonmatching_groups_for_both_label_mismatch_paths(host):
+ groups = read_yaml_file(host, "/opt/yaml_branch_matrix.yml")
+
+ assert get_group_by_labels(groups, {"job": "wrong", "env": "prod"})["targets"] == [
+ "keep_same_length:9500"
+ ]
+
+ assert get_group_by_labels(groups, {"job": "short"})["targets"] == [
+ "keep_length_mismatch:9500"
+ ]
+
+ assert get_group_by_labels(groups, {"job": "foreign"})["targets"] == ["foreign:9505"]
+
+
+def test_yaml_branch_matrix_moves_hosts_and_drops_emptied_groups(host):
+ groups = read_yaml_file(host, "/opt/yaml_branch_matrix.yml")
+
+ assert not has_group_with_labels(groups, {"job": "move_source"})
+
+ assert get_group_by_labels(groups, {"job": "move_target"})["targets"] == [
+ "application3:9502"
+ ]
+
+ assert get_group_by_labels(groups, {"job": "promoted"})["targets"] == [
+ "orphan_unlabeled:9503"
+ ]
+
+
+def test_yaml_branch_matrix_preserves_unlabeled_group_when_targets_remain(host):
+ groups = read_yaml_file(host, "/opt/yaml_branch_matrix.yml")
+
+ assert get_unlabeled_group(groups)["targets"] == [
+ "shared_unlabeled:9504",
+ "standalone_branch:9504",
+ ]
+
+
+def test_yaml_branch_matrix_creates_missing_file_groups_for_labeled_and_unlabeled_targets(host):
+ assert read_yaml_file(host, "/opt/yaml_missing_labeled.yml") == [
+ {"labels": {"team": "infra"}, "targets": ["application4:9600"]}
+ ]
+
+ assert read_yaml_file(host, "/opt/yaml_missing_unlabeled.yml") == [
+ {"targets": ["application4:9601"]}
+ ]