This is an automated email from the ASF dual-hosted git repository.
mobuchowski 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 01d0df1eaa9 Add OpenLineage parent info injection to GlueJobOperator
(#64513)
01d0df1eaa9 is described below
commit 01d0df1eaa903b5d2d65d76167b87da9db9f48a0
Author: Rahul Madan <[email protected]>
AuthorDate: Thu Apr 2 19:29:06 2026 +0530
Add OpenLineage parent info injection to GlueJobOperator (#64513)
* added feature of injecting parent info conf to glue jobs when triggered
via airfow
Signed-off-by: Rahul Madan <[email protected]>
* added next version comment for compat
Signed-off-by: Rahul Madan <[email protected]>
* fixed ci issue due to uuid
Signed-off-by: Rahul Madan <[email protected]>
* added safety null check for parent_props
Signed-off-by: Rahul Madan <[email protected]>
---------
Signed-off-by: Rahul Madan <[email protected]>
---
providers/amazon/pyproject.toml | 2 +-
.../airflow/providers/amazon/aws/operators/glue.py | 36 +++-
.../tests/unit/amazon/aws/operators/test_glue.py | 189 +++++++++++++++++++++
.../common/compat/openlineage/utils/spark.py | 20 +++
providers/openlineage/pyproject.toml | 2 +-
.../airflow/providers/openlineage/utils/spark.py | 70 ++++++++
.../tests/unit/openlineage/utils/test_spark.py | 98 +++++++++++
7 files changed, 411 insertions(+), 6 deletions(-)
diff --git a/providers/amazon/pyproject.toml b/providers/amazon/pyproject.toml
index 414cedfeabf..5b155bd19fc 100644
--- a/providers/amazon/pyproject.toml
+++ b/providers/amazon/pyproject.toml
@@ -60,7 +60,7 @@ requires-python = ">=3.10"
# After you modify the dependencies, and rebuild your Breeze CI image with
``breeze ci-image build``
dependencies = [
"apache-airflow>=2.11.0",
- "apache-airflow-providers-common-compat>=1.13.0",
+ "apache-airflow-providers-common-compat>=1.13.0", # use next version
"apache-airflow-providers-common-sql>=1.32.0",
"apache-airflow-providers-http",
# We should update minimum version of boto3 and here regularly to avoid
`pip` backtracking with the number
diff --git
a/providers/amazon/src/airflow/providers/amazon/aws/operators/glue.py
b/providers/amazon/src/airflow/providers/amazon/aws/operators/glue.py
index ce96a76b3d0..3d46aecb32e 100644
--- a/providers/amazon/src/airflow/providers/amazon/aws/operators/glue.py
+++ b/providers/amazon/src/airflow/providers/amazon/aws/operators/glue.py
@@ -35,6 +35,10 @@ from airflow.providers.amazon.aws.triggers.glue import (
)
from airflow.providers.amazon.aws.utils import validate_execute_complete_event
from airflow.providers.amazon.aws.utils.mixins import aws_template_fields
+from airflow.providers.common.compat.openlineage.utils.spark import (
+ inject_parent_job_information_into_glue_arguments,
+ inject_transport_information_into_glue_arguments,
+)
from airflow.providers.common.compat.sdk import AirflowException, conf
if TYPE_CHECKING:
@@ -78,6 +82,12 @@ class GlueJobOperator(AwsBaseOperator[GlueJobHook]):
It is recommended to set this parameter to 10 when you are using
concurrency=1.
For more information see:
https://repost.aws/questions/QUaKgpLBMPSGWO0iq2Fob_bw/glue-run-concurrent-jobs#ANFpCL2fRnQRqgDFuIU_rpvA
+ :param openlineage_inject_parent_job_info: If True, injects OpenLineage
parent job information into the
+ Glue job's ``--conf`` argument so the Glue Spark job emits a
``parentRunFacet`` linking back to the
+ Airflow task. Defaults to the
``openlineage.spark_inject_parent_job_info`` config value.
+ :param openlineage_inject_transport_info: If True, injects OpenLineage
transport configuration into the
+ Glue job's ``--conf`` argument so the Glue Spark job sends OL events
to the same backend as Airflow.
+ Defaults to the ``openlineage.spark_inject_transport_info`` config
value.
:param waiter_delay: Time in seconds to wait between status checks.
(default: 60)
:param waiter_max_attempts: Maximum number of attempts to check for job
completion. (default: 20)
:param aws_conn_id: The Airflow connection used for AWS credentials.
@@ -140,6 +150,12 @@ class GlueJobOperator(AwsBaseOperator[GlueJobHook]):
waiter_delay: int = 60,
waiter_max_attempts: int = 75,
resume_glue_job_on_retry: bool = False,
+ openlineage_inject_parent_job_info: bool = conf.getboolean(
+ "openlineage", "spark_inject_parent_job_info", fallback=False
+ ),
+ openlineage_inject_transport_info: bool = conf.getboolean(
+ "openlineage", "spark_inject_transport_info", fallback=False
+ ),
**kwargs,
):
super().__init__(**kwargs)
@@ -170,6 +186,8 @@ class GlueJobOperator(AwsBaseOperator[GlueJobHook]):
self.waiter_delay = waiter_delay
self.waiter_max_attempts = waiter_max_attempts
self.resume_glue_job_on_retry = resume_glue_job_on_retry
+ self.openlineage_inject_parent_job_info =
openlineage_inject_parent_job_info
+ self.openlineage_inject_transport_info =
openlineage_inject_transport_info
@property
def _hook_parameters(self):
@@ -220,8 +238,10 @@ class GlueJobOperator(AwsBaseOperator[GlueJobHook]):
map_index = -1
return f"{ti.dag_id}:{ti.task_id}:{ti.run_id}:{map_index}"
- def _prepare_script_args_with_task_uuid(self, context: Context) ->
tuple[dict, str]:
- script_args = dict(self.script_args or {})
+ def _prepare_script_args_with_task_uuid(
+ self, context: Context, base_args: dict | None = None
+ ) -> tuple[dict, str]:
+ script_args = dict(base_args if base_args is not None else
(self.script_args or {}))
if self.TASK_UUID_ARG in script_args:
task_uuid = str(script_args[self.TASK_UUID_ARG])
else:
@@ -254,11 +274,19 @@ class GlueJobOperator(AwsBaseOperator[GlueJobHook]):
:return: the current Glue job ID.
"""
previous_job_run_id = None
- script_args = self.script_args
+ script_args = dict(self.script_args)
task_uuid = None
+
+ if self.openlineage_inject_parent_job_info:
+ self.log.info("Injecting OpenLineage parent job information into
Glue job arguments.")
+ script_args =
inject_parent_job_information_into_glue_arguments(script_args, context)
+ if self.openlineage_inject_transport_info:
+ self.log.info("Injecting OpenLineage transport information into
Glue job arguments.")
+ script_args =
inject_transport_information_into_glue_arguments(script_args, context)
+
if self.resume_glue_job_on_retry:
ti = context["ti"]
- script_args, task_uuid =
self._prepare_script_args_with_task_uuid(context)
+ script_args, task_uuid =
self._prepare_script_args_with_task_uuid(context, base_args=script_args)
previous_job_run_id = ti.xcom_pull(key="glue_job_run_id",
task_ids=ti.task_id)
if previous_job_run_id:
try:
diff --git a/providers/amazon/tests/unit/amazon/aws/operators/test_glue.py
b/providers/amazon/tests/unit/amazon/aws/operators/test_glue.py
index e7d15fbbaa8..230193feeb4 100644
--- a/providers/amazon/tests/unit/amazon/aws/operators/test_glue.py
+++ b/providers/amazon/tests/unit/amazon/aws/operators/test_glue.py
@@ -587,6 +587,195 @@ class TestGlueJobOperator:
assert xcom_calls[0][1]["value"] == "existing_run_123"
+class TestGlueJobOperatorOpenLineageInjection:
+ """Tests for OpenLineage parent job info and transport info injection in
GlueJobOperator."""
+
+ @mock.patch.object(GlueJobHook, "get_conn")
+ @mock.patch.object(GlueJobHook, "initialize_job")
+ @mock.patch(
+
"airflow.providers.amazon.aws.operators.glue.inject_parent_job_information_into_glue_arguments"
+ )
+ def test_inject_parent_job_info_called_when_enabled(
+ self, mock_inject_parent, mock_initialize_job, mock_get_conn
+ ):
+ mock_inject_parent.side_effect = lambda args, ctx: {
+ **args,
+ "--conf": "spark.openlineage.parentJobNamespace=ns",
+ }
+ mock_initialize_job.return_value = {"JobRunState": "RUNNING",
"JobRunId": JOB_RUN_ID}
+
+ glue = GlueJobOperator(
+ task_id=TASK_ID,
+ job_name=JOB_NAME,
+ script_location="s3://folder/file",
+ iam_role_name="my_test_role",
+ wait_for_completion=False,
+ openlineage_inject_parent_job_info=True,
+ )
+ context = mock.MagicMock()
+ glue.execute(context)
+
+ mock_inject_parent.assert_called_once()
+ call_args = mock_initialize_job.call_args[0][0]
+ assert "--conf" in call_args
+ assert "spark.openlineage.parentJobNamespace=ns" in call_args["--conf"]
+
+ @mock.patch.object(GlueJobHook, "get_conn")
+ @mock.patch.object(GlueJobHook, "initialize_job")
+ @mock.patch(
+
"airflow.providers.amazon.aws.operators.glue.inject_parent_job_information_into_glue_arguments"
+ )
+ def test_inject_parent_job_info_not_called_when_disabled(
+ self, mock_inject_parent, mock_initialize_job, mock_get_conn
+ ):
+ mock_initialize_job.return_value = {"JobRunState": "RUNNING",
"JobRunId": JOB_RUN_ID}
+
+ glue = GlueJobOperator(
+ task_id=TASK_ID,
+ job_name=JOB_NAME,
+ script_location="s3://folder/file",
+ iam_role_name="my_test_role",
+ wait_for_completion=False,
+ openlineage_inject_parent_job_info=False,
+ )
+ glue.execute(mock.MagicMock())
+
+ mock_inject_parent.assert_not_called()
+
+ @mock.patch.object(GlueJobHook, "get_conn")
+ @mock.patch.object(GlueJobHook, "initialize_job")
+ @mock.patch(
+
"airflow.providers.amazon.aws.operators.glue.inject_transport_information_into_glue_arguments"
+ )
+ def test_inject_transport_info_called_when_enabled(
+ self, mock_inject_transport, mock_initialize_job, mock_get_conn
+ ):
+ mock_inject_transport.side_effect = lambda args, ctx: {
+ **args,
+ "--conf": "spark.openlineage.transport.type=http",
+ }
+ mock_initialize_job.return_value = {"JobRunState": "RUNNING",
"JobRunId": JOB_RUN_ID}
+
+ glue = GlueJobOperator(
+ task_id=TASK_ID,
+ job_name=JOB_NAME,
+ script_location="s3://folder/file",
+ iam_role_name="my_test_role",
+ wait_for_completion=False,
+ openlineage_inject_transport_info=True,
+ )
+ context = mock.MagicMock()
+ glue.execute(context)
+
+ mock_inject_transport.assert_called_once()
+ call_args = mock_initialize_job.call_args[0][0]
+ assert "--conf" in call_args
+ assert "spark.openlineage.transport.type=http" in call_args["--conf"]
+
+ @mock.patch.object(GlueJobHook, "get_conn")
+ @mock.patch.object(GlueJobHook, "initialize_job")
+ @mock.patch(
+
"airflow.providers.amazon.aws.operators.glue.inject_parent_job_information_into_glue_arguments"
+ )
+ @mock.patch(
+
"airflow.providers.amazon.aws.operators.glue.inject_transport_information_into_glue_arguments"
+ )
+ def test_inject_both_parent_and_transport_info(
+ self, mock_inject_transport, mock_inject_parent, mock_initialize_job,
mock_get_conn
+ ):
+ mock_inject_parent.side_effect = lambda args, ctx: {
+ **args,
+ "--conf": "spark.openlineage.parentJobNamespace=ns",
+ }
+ mock_inject_transport.side_effect = lambda args, ctx: {
+ **args,
+ "--conf": args.get("--conf", "") + " --conf
spark.openlineage.transport.type=http",
+ }
+ mock_initialize_job.return_value = {"JobRunState": "RUNNING",
"JobRunId": JOB_RUN_ID}
+
+ glue = GlueJobOperator(
+ task_id=TASK_ID,
+ job_name=JOB_NAME,
+ script_location="s3://folder/file",
+ iam_role_name="my_test_role",
+ wait_for_completion=False,
+ openlineage_inject_parent_job_info=True,
+ openlineage_inject_transport_info=True,
+ )
+ glue.execute(mock.MagicMock())
+
+ mock_inject_parent.assert_called_once()
+ mock_inject_transport.assert_called_once()
+ call_args = mock_initialize_job.call_args[0][0]
+ assert "spark.openlineage.parentJobNamespace=ns" in call_args["--conf"]
+ assert "spark.openlineage.transport.type=http" in call_args["--conf"]
+
+ @mock.patch.object(GlueJobHook, "get_conn")
+ @mock.patch.object(GlueJobHook, "initialize_job")
+ @mock.patch(
+
"airflow.providers.amazon.aws.operators.glue.inject_parent_job_information_into_glue_arguments"
+ )
+ def test_inject_parent_job_info_preserves_existing_script_args(
+ self, mock_inject_parent, mock_initialize_job, mock_get_conn
+ ):
+ mock_inject_parent.side_effect = lambda args, ctx: {
+ **args,
+ "--conf": "spark.openlineage.parentJobNamespace=ns",
+ }
+ mock_initialize_job.return_value = {"JobRunState": "RUNNING",
"JobRunId": JOB_RUN_ID}
+
+ glue = GlueJobOperator(
+ task_id=TASK_ID,
+ job_name=JOB_NAME,
+ script_location="s3://folder/file",
+ iam_role_name="my_test_role",
+ wait_for_completion=False,
+ openlineage_inject_parent_job_info=True,
+ script_args={"--my-arg": "my-value"},
+ )
+ glue.execute(mock.MagicMock())
+
+ call_args = mock_initialize_job.call_args[0][0]
+ assert call_args["--my-arg"] == "my-value"
+ assert "--conf" in call_args
+
+ @mock.patch.object(GlueJobHook, "get_conn")
+ @mock.patch.object(GlueJobHook, "initialize_job")
+ @mock.patch(
+
"airflow.providers.amazon.aws.operators.glue.inject_parent_job_information_into_glue_arguments"
+ )
+ def test_inject_parent_job_info_with_resume_on_retry(
+ self, mock_inject_parent, mock_initialize_job, mock_get_conn
+ ):
+ """OL injection is applied before task UUID is added; both end up in
the args passed to initialize_job."""
+ mock_inject_parent.side_effect = lambda args, ctx: {
+ **args,
+ "--conf": "spark.openlineage.parentJobNamespace=ns",
+ }
+ mock_initialize_job.return_value = {"JobRunState": "RUNNING",
"JobRunId": JOB_RUN_ID}
+
+ glue = GlueJobOperator(
+ task_id=TASK_ID,
+ job_name=JOB_NAME,
+ script_location="s3://folder/file",
+ iam_role_name="my_test_role",
+ wait_for_completion=False,
+ openlineage_inject_parent_job_info=True,
+ resume_glue_job_on_retry=True,
+ )
+
+ mock_ti = mock.MagicMock()
+ mock_ti.xcom_pull.return_value = None # no previous run
+ context = {"ti": mock_ti}
+ glue.execute(context)
+
+ mock_inject_parent.assert_called_once()
+ # The injected OL arg and the task UUID arg should both be present
+ call_args = mock_initialize_job.call_args[0][0]
+ assert "--conf" in call_args
+ assert GlueJobOperator.TASK_UUID_ARG in call_args
+
+
class TestGlueDataQualityOperator:
RULE_SET_NAME = "TestRuleSet"
RULE_SET = 'Rules=[ColumnLength "review_id" = 15]'
diff --git
a/providers/common/compat/src/airflow/providers/common/compat/openlineage/utils/spark.py
b/providers/common/compat/src/airflow/providers/common/compat/openlineage/utils/spark.py
index 1028bf3debf..d92dad56dad 100644
---
a/providers/common/compat/src/airflow/providers/common/compat/openlineage/utils/spark.py
+++
b/providers/common/compat/src/airflow/providers/common/compat/openlineage/utils/spark.py
@@ -24,13 +24,17 @@ log = logging.getLogger(__name__)
if TYPE_CHECKING:
from airflow.providers.openlineage.utils.spark import (
+ inject_parent_job_information_into_glue_arguments,
inject_parent_job_information_into_spark_properties,
+ inject_transport_information_into_glue_arguments,
inject_transport_information_into_spark_properties,
)
from airflow.sdk import Context
try:
from airflow.providers.openlineage.utils.spark import (
+ inject_parent_job_information_into_glue_arguments,
inject_parent_job_information_into_spark_properties,
+ inject_transport_information_into_glue_arguments,
inject_transport_information_into_spark_properties,
)
except ImportError:
@@ -49,8 +53,24 @@ except ImportError:
)
return properties
+ def inject_parent_job_information_into_glue_arguments(script_args: dict,
context: Context) -> dict:
+ log.warning(
+ "Could not import `airflow.providers.openlineage.plugins.macros`."
+ "Skipping the injection of OpenLineage parent job information into
Glue job arguments."
+ )
+ return script_args
+
+ def inject_transport_information_into_glue_arguments(script_args: dict,
context: Context) -> dict:
+ log.warning(
+ "Could not import
`airflow.providers.openlineage.plugins.listener`."
+ "Skipping the injection of OpenLineage transport information into
Glue job arguments."
+ )
+ return script_args
+
__all__ = [
+ "inject_parent_job_information_into_glue_arguments",
"inject_parent_job_information_into_spark_properties",
+ "inject_transport_information_into_glue_arguments",
"inject_transport_information_into_spark_properties",
]
diff --git a/providers/openlineage/pyproject.toml
b/providers/openlineage/pyproject.toml
index 5f7870c38f3..989d2d2748d 100644
--- a/providers/openlineage/pyproject.toml
+++ b/providers/openlineage/pyproject.toml
@@ -61,7 +61,7 @@ requires-python = ">=3.10"
dependencies = [
"apache-airflow>=2.11.0",
"apache-airflow-providers-common-sql>=1.32.0",
- "apache-airflow-providers-common-compat>=1.14.0",
+ "apache-airflow-providers-common-compat>=1.14.0", # use next version
"attrs>=22.2",
"openlineage-integration-common>=1.41.0",
"openlineage-python>=1.41.0",
diff --git
a/providers/openlineage/src/airflow/providers/openlineage/utils/spark.py
b/providers/openlineage/src/airflow/providers/openlineage/utils/spark.py
index a92ac25eab2..837946fecbe 100644
--- a/providers/openlineage/src/airflow/providers/openlineage/utils/spark.py
+++ b/providers/openlineage/src/airflow/providers/openlineage/utils/spark.py
@@ -195,3 +195,73 @@ def
inject_transport_information_into_spark_properties(properties: dict, context
return properties
return {**properties, **_get_transport_information_as_spark_properties()}
+
+
+def inject_parent_job_information_into_glue_arguments(script_args: dict,
context: Context) -> dict:
+ """
+ Inject parent job information into Glue job arguments if not already
present.
+
+ Glue jobs pass Spark properties via the ``--conf`` key in the script_args
dict.
+ Multiple Spark conf properties are combined into the ``--conf`` key value
with
+ ``' --conf '`` as separator between each property assignment.
+
+ Args:
+ script_args: Glue job script arguments dict (maps to boto3
``Arguments``).
+ context: The context containing task instance information.
+
+ Returns:
+ Modified script_args dict with OpenLineage parent job information
injected, if applicable.
+ """
+ existing_conf = script_args.get("--conf", "")
+
+ if "spark.openlineage.parent" in existing_conf:
+ log.info(
+ "Some OpenLineage properties with parent job information are
already present "
+ "in Glue job arguments. Skipping the injection of OpenLineage "
+ "parent job information into Glue job arguments."
+ )
+ return script_args
+
+ parent_props = _get_parent_job_information_as_spark_properties(context)
+ if not parent_props:
+ return script_args
+
+ new_conf_parts = " --conf ".join(f"{k}={v}" for k, v in
parent_props.items())
+
+ combined_conf = f"{existing_conf} --conf {new_conf_parts}" if
existing_conf else new_conf_parts
+ return {**script_args, "--conf": combined_conf}
+
+
+def inject_transport_information_into_glue_arguments(script_args: dict,
context: Context) -> dict:
+ """
+ Inject transport information into Glue job arguments if not already
present.
+
+ Glue jobs pass Spark properties via the ``--conf`` key in the script_args
dict.
+ Multiple Spark conf properties are combined into the ``--conf`` key value
with
+ ``' --conf '`` as separator between each property assignment.
+
+ Args:
+ script_args: Glue job script arguments dict (maps to boto3
``Arguments``).
+ context: The context containing task instance information.
+
+ Returns:
+ Modified script_args dict with OpenLineage transport information
injected, if applicable.
+ """
+ existing_conf = script_args.get("--conf", "")
+
+ if "spark.openlineage.transport" in existing_conf:
+ log.info(
+ "Some OpenLineage properties with transport information are
already present "
+ "in Glue job arguments. Skipping the injection of OpenLineage "
+ "transport information into Glue job arguments."
+ )
+ return script_args
+
+ transport_props = _get_transport_information_as_spark_properties()
+ if not transport_props:
+ return script_args
+
+ new_conf_parts = " --conf ".join(f"{k}={v}" for k, v in
transport_props.items())
+
+ combined_conf = f"{existing_conf} --conf {new_conf_parts}" if
existing_conf else new_conf_parts
+ return {**script_args, "--conf": combined_conf}
diff --git a/providers/openlineage/tests/unit/openlineage/utils/test_spark.py
b/providers/openlineage/tests/unit/openlineage/utils/test_spark.py
index 3dadf68482d..be835d6a429 100644
--- a/providers/openlineage/tests/unit/openlineage/utils/test_spark.py
+++ b/providers/openlineage/tests/unit/openlineage/utils/test_spark.py
@@ -31,7 +31,9 @@ from airflow.providers.openlineage.utils.spark import (
_get_transport_information_as_spark_properties,
_is_parent_job_information_present_in_spark_properties,
_is_transport_information_present_in_spark_properties,
+ inject_parent_job_information_into_glue_arguments,
inject_parent_job_information_into_spark_properties,
+ inject_transport_information_into_glue_arguments,
inject_transport_information_into_spark_properties,
)
@@ -364,3 +366,99 @@ def
test_inject_composite_transport_information_into_spark_properties(
result = inject_transport_information_into_spark_properties(properties,
EXAMPLE_CONTEXT)
expected = {**properties, **EXAMPLE_COMPOSITE_TRANSPORT_SPARK_PROPERTIES}
if should_inject else properties
assert result == expected
+
+
+# ---------------------------------------------------------------------------
+# Glue argument injection tests
+# ---------------------------------------------------------------------------
+
+
+@patch("airflow.providers.openlineage.utils.spark._get_parent_job_information_as_spark_properties")
+def
test_inject_parent_job_information_into_glue_arguments_empty_args(mock_get_parent):
+ """With no existing --conf, parent props are joined into a new --conf
value."""
+ mock_get_parent.return_value = {
+ "spark.openlineage.parentJobNamespace": "ns",
+ "spark.openlineage.parentJobName": "dag.task",
+ }
+ result = inject_parent_job_information_into_glue_arguments({},
EXAMPLE_CONTEXT)
+ assert "--conf" in result
+ conf = result["--conf"]
+ assert "spark.openlineage.parentJobNamespace=ns" in conf
+ assert "spark.openlineage.parentJobName=dag.task" in conf
+ # Multiple props joined with ' --conf '
+ assert " --conf " in conf
+
+
+@patch("airflow.providers.openlineage.utils.spark._get_parent_job_information_as_spark_properties")
+def
test_inject_parent_job_information_into_glue_arguments_appends_to_existing_conf(mock_get_parent):
+ """Existing --conf value is preserved and OL props are appended."""
+ mock_get_parent.return_value = {"spark.openlineage.parentJobNamespace":
"ns"}
+ script_args = {"--conf": "spark.some.existing=val", "--other": "arg"}
+ result = inject_parent_job_information_into_glue_arguments(script_args,
EXAMPLE_CONTEXT)
+ assert result["--other"] == "arg"
+ conf = result["--conf"]
+ assert conf.startswith("spark.some.existing=val")
+ assert "spark.openlineage.parentJobNamespace=ns" in conf
+
+
+@patch("airflow.providers.openlineage.utils.spark._get_parent_job_information_as_spark_properties")
+def
test_inject_parent_job_information_into_glue_arguments_skips_if_already_present(mock_get_parent):
+ """Injection is skipped when parent job info is already in --conf."""
+ mock_get_parent.return_value = {"spark.openlineage.parentJobNamespace":
"ns"}
+ existing = "spark.openlineage.parentJobNamespace=already_there"
+ script_args = {"--conf": existing}
+ result = inject_parent_job_information_into_glue_arguments(script_args,
EXAMPLE_CONTEXT)
+ assert result["--conf"] == existing
+ mock_get_parent.assert_not_called()
+
+
+@patch("airflow.providers.openlineage.utils.spark._get_parent_job_information_as_spark_properties")
+def
test_inject_parent_job_information_into_glue_arguments_does_not_mutate_input(mock_get_parent):
+ """The original script_args dict is not mutated."""
+ mock_get_parent.return_value = {"spark.openlineage.parentJobNamespace":
"ns"}
+ original = {"--my-arg": "val"}
+ original_copy = dict(original)
+ inject_parent_job_information_into_glue_arguments(original,
EXAMPLE_CONTEXT)
+ assert original == original_copy
+
+
+@patch("airflow.providers.openlineage.utils.spark.get_openlineage_listener")
+def
test_inject_transport_information_into_glue_arguments_empty_args(mock_ol_listener):
+ """With no existing --conf, transport props are joined into a new --conf
value."""
+ fake_listener = mock.MagicMock()
+ mock_ol_listener.return_value = fake_listener
+
fake_listener.adapter.get_or_create_openlineage_client.return_value.transport =
HttpTransport(
+ HttpConfig.from_dict(EXAMPLE_HTTP_TRANSPORT_CONFIG)
+ )
+ result = inject_transport_information_into_glue_arguments({},
EXAMPLE_CONTEXT)
+ assert "--conf" in result
+ conf = result["--conf"]
+ assert "spark.openlineage.transport.type=http" in conf
+ assert "spark.openlineage.transport.url=https://some-custom.url" in conf
+
+
+@patch("airflow.providers.openlineage.utils.spark.get_openlineage_listener")
+def
test_inject_transport_information_into_glue_arguments_appends_to_existing_conf(mock_ol_listener):
+ """Existing --conf value is preserved and transport props are appended."""
+ fake_listener = mock.MagicMock()
+ mock_ol_listener.return_value = fake_listener
+
fake_listener.adapter.get_or_create_openlineage_client.return_value.transport =
HttpTransport(
+ HttpConfig.from_dict(EXAMPLE_HTTP_TRANSPORT_CONFIG)
+ )
+ script_args = {"--conf": "spark.some.existing=val"}
+ result = inject_transport_information_into_glue_arguments(script_args,
EXAMPLE_CONTEXT)
+ conf = result["--conf"]
+ assert conf.startswith("spark.some.existing=val")
+ assert "spark.openlineage.transport.type=http" in conf
+
+
+@patch("airflow.providers.openlineage.utils.spark.get_openlineage_listener")
+def
test_inject_transport_information_into_glue_arguments_skips_if_already_present(mock_ol_listener):
+ """Injection is skipped when transport info is already in --conf."""
+ fake_listener = mock.MagicMock()
+ mock_ol_listener.return_value = fake_listener
+ existing = "spark.openlineage.transport.type=http"
+ script_args = {"--conf": existing}
+ result = inject_transport_information_into_glue_arguments(script_args,
EXAMPLE_CONTEXT)
+ assert result["--conf"] == existing
+ fake_listener.adapter.get_or_create_openlineage_client.assert_not_called()