This is an automated email from the ASF dual-hosted git repository.
potiuk 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 19e86725d60 Fix DockerOperator init crash on dict mount entries
(#66553)
19e86725d60 is described below
commit 19e86725d60dc3862ab9b2685bd22016323ca41c
Author: Dov Benyomin Sohacheski <[email protected]>
AuthorDate: Sun May 10 20:35:14 2026 +0300
Fix DockerOperator init crash on dict mount entries (#66553)
* Fix DockerOperator init crash on dict mount entries
PR #52451 added a loop in ``DockerOperator.__init__`` that assigns
``template_fields`` on every entry of ``mounts``. Plain ``dict`` entries
have no ``__dict__`` slot, so the assignment raises::
AttributeError: 'dict' object has no attribute 'template_fields' and
no __dict__ for setting new attributes
at construction time, breaking any DAG that historically passed mount
entries as ``dict`` (a documented and previously supported form).
Fixes the regression by normalising each entry to ``docker.types.Mount``
on input — ``Mount`` instances are passed through, and ``dict`` entries
are unpacked into ``Mount(**entry)``. The subsequent ``template_fields``
assignment then operates on a uniform ``Mount`` (a ``dict`` subclass
that does carry an instance ``__dict__``).
Closes: #66345
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
* Fixed lint
* fix lint
* Annotate self.mounts as list[Mount] to satisfy mypy
---------
Co-authored-by: Claude Opus 4.7 (1M context) <[email protected]>
---
.../airflow/providers/docker/operators/docker.py | 13 +++++++---
.../tests/unit/docker/operators/test_docker.py | 30 ++++++++++++++++++++++
2 files changed, 39 insertions(+), 4 deletions(-)
diff --git a/providers/docker/src/airflow/providers/docker/operators/docker.py
b/providers/docker/src/airflow/providers/docker/operators/docker.py
index 345a6c624f1..e81579a4f1c 100644
--- a/providers/docker/src/airflow/providers/docker/operators/docker.py
+++ b/providers/docker/src/airflow/providers/docker/operators/docker.py
@@ -151,8 +151,12 @@ class DockerOperator(BaseOperator):
The path is also made available via the environment variable
``AIRFLOW_TMP_DIR`` inside the container.
:param user: Default user inside the docker container.
- :param mounts: List of volumes to mount into the container. Each item
should
- be a :py:class:`docker.types.Mount` instance. (templated)
+ :param mounts: List of volumes to mount into the container. Each item may
+ be a :py:class:`docker.types.Mount` instance, or a ``dict`` of
+ :py:class:`~docker.types.Mount` keyword arguments (e.g.
+ ``{"target": "/data", "source": "vol", "type": "volume"}``); ``dict``
+ entries are converted to ``Mount`` instances at construction time.
+ (templated)
:param entrypoint: Overwrite the default ENTRYPOINT of the image
:param working_dir: Working directory to
set on the container (equivalent to the -w switch the docker client)
@@ -245,7 +249,7 @@ class DockerOperator(BaseOperator):
mount_tmp_dir: bool = True,
tmp_dir: str = "/tmp/airflow",
user: str | int | None = None,
- mounts: list[Mount] | None = None,
+ mounts: list[Mount | dict] | None = None,
entrypoint: str | list[str] | None = None,
working_dir: str | None = None,
xcom_all: bool = False,
@@ -304,7 +308,8 @@ class DockerOperator(BaseOperator):
self.mount_tmp_dir = mount_tmp_dir
self.tmp_dir = tmp_dir
self.user = user
- self.mounts = mounts or []
+ mounts = [mount if isinstance(mount, Mount) else Mount(**mount) for
mount in (mounts or [])]
+ self.mounts: list[Mount] = mounts
for mount in self.mounts:
mount.template_fields = ("Source", "Target", "Type")
self.entrypoint = entrypoint
diff --git a/providers/docker/tests/unit/docker/operators/test_docker.py
b/providers/docker/tests/unit/docker/operators/test_docker.py
index d375bd577ce..a753894d48f 100644
--- a/providers/docker/tests/unit/docker/operators/test_docker.py
+++ b/providers/docker/tests/unit/docker/operators/test_docker.py
@@ -818,3 +818,33 @@ class TestDockerOperator:
rendered = ti.render_templates()
assert rendered.container_name == f"python_{ti.dag_id}"
assert rendered.mounts[0]["Target"] == f"/{ti.run_id}"
+
+ def test_dict_mounts_are_normalized_to_mount_objects(self):
+ op = DockerOperator(
+ task_id="test",
+ image="test",
+ mounts=[
+ {"target": "/data", "source": "workspace", "type": "volume",
"read_only": False},
+ Mount(target="/logs", source="logs", type="volume"),
+ ],
+ )
+ assert all(isinstance(m, Mount) for m in op.mounts)
+ assert op.mounts[0]["Target"] == "/data"
+ assert op.mounts[0]["Source"] == "workspace"
+ assert op.mounts[0]["Type"] == "volume"
+ assert op.mounts[0]["ReadOnly"] is False
+ assert op.mounts[1]["Target"] == "/logs"
+
+ @pytest.mark.db_test
+ def test_dict_mounts_are_templated(self, create_task_instance_of_operator):
+ ti = create_task_instance_of_operator(
+ operator_class=DockerOperator,
+ dag_id="test",
+ task_id="test",
+ image="test",
+ mounts=[
+ {"target": "/{{task_instance.run_id}}", "source": "workspace",
"type": "volume"},
+ ],
+ )
+ rendered = ti.render_templates()
+ assert rendered.mounts[0]["Target"] == f"/{ti.run_id}"