This is an automated email from the ASF dual-hosted git repository.
amoghdesai pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/airflow.git
The following commit(s) were added to refs/heads/main by this push:
new e0cd6e246c2 SQL not rendered in Rendered Templates view (#60739)
e0cd6e246c2 is described below
commit e0cd6e246c288d33f359ec2268b3d342832e9648
Author: Sarthak Vaish <[email protected]>
AuthorDate: Mon Feb 23 15:41:18 2026 +0530
SQL not rendered in Rendered Templates view (#60739)
---
.../src/airflow/sdk/execution_time/task_runner.py | 34 +++++++++-
.../task_sdk/execution_time/test_task_runner.py | 72 ++++++++++++++++++++++
2 files changed, 103 insertions(+), 3 deletions(-)
diff --git a/task-sdk/src/airflow/sdk/execution_time/task_runner.py
b/task-sdk/src/airflow/sdk/execution_time/task_runner.py
index 3d641c36fe0..7355f886ee8 100644
--- a/task-sdk/src/airflow/sdk/execution_time/task_runner.py
+++ b/task-sdk/src/airflow/sdk/execution_time/task_runner.py
@@ -1005,16 +1005,44 @@ def _serialize_template_field(template_field: Any,
name: str) -> str | dict | li
def _serialize_rendered_fields(task: AbstractOperator) -> dict[str, JsonValue]:
from airflow.sdk._shared.secrets_masker import redact
- rendered_fields = {}
+ rendered_fields: dict[str, JsonValue] = {}
for field in task.template_fields:
value = getattr(task, field)
serialized = _serialize_template_field(value, field)
# Redact secrets in the task process itself before sending to API
server
# This ensures that the secrets those are registered via mask_secret()
on workers / dag processor are properly masked
# on the UI.
- rendered_fields[field] = redact(serialized, field)
+ redacted = redact(serialized, field)
+ rendered_fields[field] =
TypeAdapter(JsonValue).validate_python(redacted)
+
+ renderers = getattr(task, "template_fields_renderers", {})
+ for renderer_path in renderers:
+ if "." not in renderer_path:
+ continue
+
+ base_field, _, remainder = renderer_path.partition(".")
+ if base_field not in task.template_fields:
+ continue
+
+ base_value = getattr(task, base_field, None)
+ if base_value is None:
+ continue
+
+ current = base_value
+ for key in remainder.split("."):
+ if isinstance(current, dict):
+ current = current.get(key)
+ else:
+ current = getattr(current, key, None)
+ if current is None:
+ break
+
+ if current is not None:
+ serialized = _serialize_template_field(current, renderer_path)
+ redacted = redact(serialized, renderer_path)
+ rendered_fields[renderer_path] =
TypeAdapter(JsonValue).validate_python(redacted)
- return rendered_fields # type: ignore[return-value] # Convince mypy that
this is OK since we pass JsonValue to redact, so it will return the same
+ return rendered_fields
def _build_asset_profiles(lineage_objects: list) -> Iterator[AssetProfile]:
diff --git a/task-sdk/tests/task_sdk/execution_time/test_task_runner.py
b/task-sdk/tests/task_sdk/execution_time/test_task_runner.py
index 3de79732ff5..f40b4399cff 100644
--- a/task-sdk/tests/task_sdk/execution_time/test_task_runner.py
+++ b/task-sdk/tests/task_sdk/execution_time/test_task_runner.py
@@ -2748,6 +2748,78 @@ class TestRuntimeTaskInstance:
== '[{"name": "var1", "value": "This is a test phrase.",
"value_from": null}, {"name": "var2", "value": "***", "value_from": null},
{"name": "var3", "value": "***", "value_from": null}]'
)
+ def test_nested_template_field_renderer_respects_redaction(
+ self, create_runtime_ti, mock_supervisor_comms
+ ):
+ """
+ Ensure nested template_fields_renderers paths still serialize
+ and redact correctly.
+ """
+
+ class CustomOperator(BaseOperator):
+ template_fields = ("config",)
+ template_fields_renderers = {"config.nested.secret": "json"}
+
+ def __init__(self, config, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self.config = config
+
+ def execute(self, context):
+ pass
+
+ config = {"nested": {"secret": "top_secret"}}
+
+ task = CustomOperator(
+ task_id="nested_redact_test",
+ config=config,
+ )
+
+ runtime_ti = create_runtime_ti(task=task)
+
+ with mock.patch(
+ "airflow.sdk._shared.secrets_masker.redact",
+ side_effect=lambda val, _: f"MASKED:{val}",
+ ):
+ run(runtime_ti, context=runtime_ti.get_template_context(),
log=mock.MagicMock())
+
+ assert any(
+ "config.nested.secret" in call.kwargs.get("msg").rendered_fields
+ for call in mock_supervisor_comms.send.mock_calls
+ if hasattr(call.kwargs.get("msg"), "rendered_fields")
+ )
+
+ def test_rendered_fields_validates_json_value_types(self,
create_runtime_ti, mock_supervisor_comms):
+ """
+ Ensure validated JSON-compatible types are preserved after redaction.
+ """
+
+ class CustomOperator(BaseOperator):
+ template_fields = ("data",)
+
+ def __init__(self, data, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self.data = data
+
+ def execute(self, context):
+ pass
+
+ complex_value = {"key": [1, 2, {"nested": "value"}]}
+
+ task = CustomOperator(task_id="json_validation_test",
data=complex_value)
+ runtime_ti = create_runtime_ti(task=task)
+
+ with mock.patch(
+ "airflow.sdk._shared.secrets_masker.redact",
+ side_effect=lambda val, _: val,
+ ):
+ run(runtime_ti, context=runtime_ti.get_template_context(),
log=mock.MagicMock())
+
+ assert any(
+ call.kwargs.get("msg").rendered_fields["data"] == complex_value
+ for call in mock_supervisor_comms.send.mock_calls
+ if hasattr(call.kwargs.get("msg"), "rendered_fields")
+ )
+
class TestXComAfterTaskExecution:
@pytest.mark.parametrize(