From 7ac361a9cc35b39b166e5f3afaef4971c43ba013 Mon Sep 17 00:00:00 2001 From: "Jonas L." Date: Fri, 31 Oct 2025 12:36:19 +0100 Subject: [PATCH] feat: add `txt_record` filter to format TXT records (#721) ##### SUMMARY The format of TXT records must consist of one or many quoted strings of 255 characters. Use this function to format TXT record that must match the format required by the API: ```yml - name: Create a SPF record hetzner.hcloud.zone_rrset: zone: example.com name: "@" type: "TXT" records: - value: "{{ 'v=spf1 include:_spf.example.net ~all' | hetzner.hcloud.text_record }}" state: present ``` ##### ISSUE TYPE - Feature Pull Request ##### COMPONENT NAME zone_rrset --- changelogs/fragments/txt-record-filter.yml | 2 ++ plugins/filter/all.py | 20 ++++++++++++++++++++ plugins/filter/txt_record.yml | 19 +++++++++++++++++++ plugins/modules/zone_rrset.py | 9 +++++++++ tests/unit/filter/test_all.py | 17 ++++++++++++++++- 5 files changed, 66 insertions(+), 1 deletion(-) create mode 100644 changelogs/fragments/txt-record-filter.yml create mode 100644 plugins/filter/txt_record.yml diff --git a/changelogs/fragments/txt-record-filter.yml b/changelogs/fragments/txt-record-filter.yml new file mode 100644 index 0000000..e58c383 --- /dev/null +++ b/changelogs/fragments/txt-record-filter.yml @@ -0,0 +1,2 @@ +minor_changes: + - txt_record - Add new txt_record filter to help format TXT , e.g. ``"{{ 'v=spf1 include:_spf.example.net ~all' | hetzner.hcloud.txt_record }}"``. diff --git a/plugins/filter/all.py b/plugins/filter/all.py index 2981311..6dea3f8 100644 --- a/plugins/filter/all.py +++ b/plugins/filter/all.py @@ -47,6 +47,25 @@ def load_balancer_status(load_balancer: dict, *args, **kwargs) -> Literal["unkno raise AnsibleFilterError(f"load_balancer_status - {to_native(exc)}", orig_exc=exc) from exc +# pylint: disable=unused-argument +def txt_record(record: str, *args, **kwargs) -> str: + """ + Return the status of a Load Balancer based on its targets. + """ + try: + record = record.replace('"', '\\"') + + parts = [] + for start in range(0, len(record), 255): + end = min(start + 255, len(record)) + parts.append('"' + record[start:end] + '"') + record = " ".join(parts) + + return record + except Exception as exc: + raise AnsibleFilterError(f"txt_record - {to_native(exc)}", orig_exc=exc) from exc + + class FilterModule: """ Hetzner Cloud filters. @@ -55,4 +74,5 @@ class FilterModule: def filters(self): return { "load_balancer_status": load_balancer_status, + "txt_record": txt_record, } diff --git a/plugins/filter/txt_record.yml b/plugins/filter/txt_record.yml new file mode 100644 index 0000000..1041959 --- /dev/null +++ b/plugins/filter/txt_record.yml @@ -0,0 +1,19 @@ +DOCUMENTATION: + name: txt_record + version_added: 6.1.0 + short_description: Format a TXT record + description: + - Format a TXT record by splitting it in quoted strings of 255 characters. + options: + _input: + description: Record value to format. + type: string + required: true +EXAMPLES: | + # Format a TXT record + {{ 'v=spf1 include:_spf.example.net ~all' | hetzner.hcloud.txt_record }} + +RETURN: + _value: + description: The formatted TXT record. + type: string diff --git a/plugins/modules/zone_rrset.py b/plugins/modules/zone_rrset.py index 895315f..aba1e86 100644 --- a/plugins/modules/zone_rrset.py +++ b/plugins/modules/zone_rrset.py @@ -93,6 +93,15 @@ EXAMPLES = """ comment: web server 2 state: present +- name: Create a TXT record + hetzner.hcloud.zone_rrset: + zone: example.com + name: "@" + type: "TXT" + records: + - value: "{{ 'v=spf1 include:_spf.example.net ~all' | hetzner.hcloud.txt_record }}" + state: present + - name: Delete a Zone RRSet hetzner.hcloud.zone_rrset: zone: 42 diff --git a/tests/unit/filter/test_all.py b/tests/unit/filter/test_all.py index 1e1be07..ea3232e 100644 --- a/tests/unit/filter/test_all.py +++ b/tests/unit/filter/test_all.py @@ -2,7 +2,7 @@ from __future__ import annotations import pytest -from plugins.filter.all import load_balancer_status +from plugins.filter.all import load_balancer_status, txt_record def _lb_target_server(status: str) -> dict: @@ -43,3 +43,18 @@ LOAD_BALANCER_STATUS_TEST_CASES = ( @pytest.mark.parametrize(("value", "expected"), LOAD_BALANCER_STATUS_TEST_CASES) def test_load_balancer_status(value, expected): assert expected == load_balancer_status(value) + + +manyA = "a" * 255 +someB = "b" * 10 + +TXT_RECORD_TEST_CASES = ( + ("hello world", '"hello world"'), + ('hello "world"', '"hello \\"world\\""'), + (manyA + someB, f'"{manyA}" "{someB}"'), +) + + +@pytest.mark.parametrize(("value", "expected"), TXT_RECORD_TEST_CASES) +def test_txt_record(value, expected): + assert expected == txt_record(value)