From fe31a04f3d02c5da1a0dbddf377a10eed6266a91 Mon Sep 17 00:00:00 2001
From: Felix Fontein <felix@fontein.de>
Date: Sun, 9 Mar 2025 16:03:39 +0100
Subject: [PATCH] Vendor and use internal code from ansible-core to fix YAML
 callback.

Ref: https://github.com/ansible/ansible/issues/84781
---
 changelogs/fragments/9833-data-tagging.yml |  1 +
 plugins/callback/yaml.py                   | 94 ++++++++++++++++------
 2 files changed, 72 insertions(+), 23 deletions(-)

diff --git a/changelogs/fragments/9833-data-tagging.yml b/changelogs/fragments/9833-data-tagging.yml
index 7d4617b7f9..a3f0cb2d25 100644
--- a/changelogs/fragments/9833-data-tagging.yml
+++ b/changelogs/fragments/9833-data-tagging.yml
@@ -2,6 +2,7 @@ bugfixes:
   - "dependent look plugin - make compatible with ansible-core's Data Tagging feature (https://github.com/ansible-collections/community.general/pull/9833)."
   - "reveal_ansible_type filter plugin and ansible_type test plugin - make compatible with ansible-core's Data Tagging feature (https://github.com/ansible-collections/community.general/pull/9833)."
   - "diy callback plugin - make compatible with ansible-core's Data Tagging feature (https://github.com/ansible-collections/community.general/pull/9833)."
+  - "yaml callback plugin - use ansible-core internals to avoid breakage with Data Tagging (https://github.com/ansible-collections/community.general/pull/9833)."
 known_issues:
   - "reveal_ansible_type filter plugin and ansible_type test plugin - note that ansible-core's Data Tagging feature implements new aliases,
      such as ``_AnsibleTaggedStr`` for ``str``, ``_AnsibleTaggedInt`` for ``int``, and ``_AnsibleTaggedFloat`` for ``float``
diff --git a/plugins/callback/yaml.py b/plugins/callback/yaml.py
index 25c797e236..3393e363d5 100644
--- a/plugins/callback/yaml.py
+++ b/plugins/callback/yaml.py
@@ -53,29 +53,77 @@ def should_use_block(value):
     return False
 
 
-class MyDumper(AnsibleDumper):
-    def represent_scalar(self, tag, value, style=None):
-        """Uses block style for multi-line strings"""
-        if style is None:
-            if should_use_block(value):
-                style = '|'
-                # we care more about readable than accuracy, so...
-                # ...no trailing space
-                value = value.rstrip()
-                # ...and non-printable characters
-                value = ''.join(x for x in value if x in string.printable or ord(x) >= 0xA0)
-                # ...tabs prevent blocks from expanding
-                value = value.expandtabs()
-                # ...and odd bits of whitespace
-                value = re.sub(r'[\x0b\x0c\r]', '', value)
-                # ...as does trailing space
-                value = re.sub(r' +\n', '\n', value)
-            else:
-                style = self.default_style
-        node = yaml.representer.ScalarNode(tag, value, style=style)
-        if self.alias_key is not None:
-            self.represented_objects[self.alias_key] = node
-        return node
+try:
+    class MyDumper(AnsibleDumper):  # pylint: disable=inherit-non-class
+        def represent_scalar(self, tag, value, style=None):
+            """Uses block style for multi-line strings"""
+            if style is None:
+                if should_use_block(value):
+                    style = '|'
+                    # we care more about readable than accuracy, so...
+                    # ...no trailing space
+                    value = value.rstrip()
+                    # ...and non-printable characters
+                    value = ''.join(x for x in value if x in string.printable or ord(x) >= 0xA0)
+                    # ...tabs prevent blocks from expanding
+                    value = value.expandtabs()
+                    # ...and odd bits of whitespace
+                    value = re.sub(r'[\x0b\x0c\r]', '', value)
+                    # ...as does trailing space
+                    value = re.sub(r' +\n', '\n', value)
+                else:
+                    style = self.default_style
+            node = yaml.representer.ScalarNode(tag, value, style=style)
+            if self.alias_key is not None:
+                self.represented_objects[self.alias_key] = node
+            return node
+except:  # noqa: E722, pylint: disable=bare-except
+    # This happens with Data Tagging, see https://github.com/ansible/ansible/issues/84781
+    # Until there is a better solution we'll resort to using ansible-core internals.
+    from ansible._internal._yaml import _dumper
+    import typing as t
+
+    class MyDumper(_dumper._BaseDumper):
+        # This code is mostly taken from ansible._internal._yaml._dumper
+        @classmethod
+        def _register_representers(cls) -> None:
+            cls.add_multi_representer(_dumper.AnsibleTaggedObject, cls.represent_ansible_tagged_object)
+            cls.add_multi_representer(_dumper.Tripwire, cls.represent_tripwire)
+            cls.add_multi_representer(_dumper.c.Mapping, _dumper.SafeRepresenter.represent_dict)
+            cls.add_multi_representer(_dumper.c.Sequence, _dumper.SafeRepresenter.represent_list)
+
+        def represent_ansible_tagged_object(self, data):
+            if ciphertext := _dumper.VaultHelper.get_ciphertext(data, with_tags=False):
+                return self.represent_scalar('!vault', ciphertext, style='|')
+
+            return self.represent_data(_dumper.AnsibleTagHelper.as_native_type(data))  # automatically decrypts encrypted strings
+
+        def represent_tripwire(self, data: _dumper.Tripwire) -> t.NoReturn:
+            data.trip()
+
+        # The following function is the same as in the try/except
+        def represent_scalar(self, tag, value, style=None):
+            """Uses block style for multi-line strings"""
+            if style is None:
+                if should_use_block(value):
+                    style = '|'
+                    # we care more about readable than accuracy, so...
+                    # ...no trailing space
+                    value = value.rstrip()
+                    # ...and non-printable characters
+                    value = ''.join(x for x in value if x in string.printable or ord(x) >= 0xA0)
+                    # ...tabs prevent blocks from expanding
+                    value = value.expandtabs()
+                    # ...and odd bits of whitespace
+                    value = re.sub(r'[\x0b\x0c\r]', '', value)
+                    # ...as does trailing space
+                    value = re.sub(r' +\n', '\n', value)
+                else:
+                    style = self.default_style
+            node = yaml.representer.ScalarNode(tag, value, style=style)
+            if self.alias_key is not None:
+                self.represented_objects[self.alias_key] = node
+            return node
 
 
 class CallbackModule(Default):